<!DOCTYPE html>
<html lang="en-US" dir="ltr">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>秒杀思路 | VitePress</title>
    <meta name="description" content="A VitePress site">
    <link rel="preload stylesheet" href="/notebook/assets/style.3dbfd0c2.css" as="style">
    
    <script type="module" src="/notebook/assets/app.8aaa4cbe.js"></script>
    <link rel="preload" href="/notebook/assets/inter-roman-latin.2ed14f66.woff2" as="font" type="font/woff2" crossorigin="">
    <link rel="modulepreload" href="/notebook/assets/chunks/framework.1336c4e5.js">
    <link rel="modulepreload" href="/notebook/assets/chunks/theme.20cddc0c.js">
    <link rel="modulepreload" href="/notebook/assets/三高_秒杀.md.3878bb64.lean.js">
    <script id="check-dark-light">(()=>{const e=localStorage.getItem("vitepress-theme-appearance")||"",a=window.matchMedia("(prefers-color-scheme: dark)").matches;(!e||e==="auto"?a:e==="dark")&&document.documentElement.classList.add("dark")})();</script>
  </head>
  <body>
    <div id="app"><div class="Layout" data-v-255ec12d><!--[--><!--]--><!--[--><span tabindex="-1" data-v-ae3e3f51></span><a href="#VPContent" class="VPSkipLink visually-hidden" data-v-ae3e3f51> Skip to content </a><!--]--><!----><header class="VPNav" data-v-255ec12d data-v-7e5bc4a5><div class="VPNavBar has-sidebar" data-v-7e5bc4a5 data-v-0937f67c><div class="container" data-v-0937f67c><div class="title" data-v-0937f67c><div class="VPNavBarTitle has-sidebar" data-v-0937f67c data-v-86d1bed8><a class="title" href="/notebook/" data-v-86d1bed8><!--[--><!--]--><!--[--><img class="VPImage logo" src="/notebook/Vue.png" alt data-v-8426fc1a><!--]--><!--[-->任硕的文档<!--]--><!--[--><!--]--></a></div></div><div class="content" data-v-0937f67c><div class="curtain" data-v-0937f67c></div><div class="content-body" data-v-0937f67c><!--[--><!--]--><div class="VPNavBarSearch search" style="--vp-meta-key:&#39;Meta&#39;;" data-v-0937f67c><!--[--><!----><div id="docsearch"><button type="button" class="DocSearch DocSearch-Button" aria-label="Search"><span class="DocSearch-Button-Container"><svg class="DocSearch-Search-Icon" width="20" height="20" viewBox="0 0 20 20" aria-label="search icon"><path d="M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z" stroke="currentColor" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"></path></svg><span class="DocSearch-Button-Placeholder">Search</span></span><span class="DocSearch-Button-Keys"><kbd class="DocSearch-Button-Key"></kbd><kbd class="DocSearch-Button-Key">K</kbd></span></button></div><!--]--></div><nav aria-labelledby="main-nav-aria-label" class="VPNavBarMenu menu" data-v-0937f67c data-v-7f418b0f><span id="main-nav-aria-label" class="visually-hidden" data-v-7f418b0f>Main Navigation</span><!--[--><!--[--><div class="VPFlyout VPNavBarMenuGroup" data-v-7f418b0f data-v-a7b5672a><button type="button" class="button" aria-haspopup="true" aria-expanded="false" data-v-a7b5672a><span class="text" data-v-a7b5672a><!----><span data-v-a7b5672a>Java学前端</span><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="text-icon" data-v-a7b5672a><path d="M12,16c-0.3,0-0.5-0.1-0.7-0.3l-6-6c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l5.3,5.3l5.3-5.3c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-6,6C12.5,15.9,12.3,16,12,16z"></path></svg></span></button><div class="menu" data-v-a7b5672a><div class="VPMenu" data-v-a7b5672a data-v-e7ea1737><div class="items" data-v-e7ea1737><!--[--><!--[--><div class="VPMenuGroup" data-v-e7ea1737 data-v-69e747b5><!----><!--[--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/Java%E5%AD%A6%E5%89%8D%E7%AB%AF/HTML+JS.html" data-v-2f2cfafc><!--[-->HTML+JS<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/Java%E5%AD%A6%E5%89%8D%E7%AB%AF/CSS.html" data-v-2f2cfafc><!--[-->CSS<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/Java%E5%AD%A6%E5%89%8D%E7%AB%AF/Vue2+%E7%BB%84%E4%BB%B6.html" data-v-2f2cfafc><!--[-->Vue2+组件<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/Java%E5%AD%A6%E5%89%8D%E7%AB%AF/Vue3+%E7%BB%84%E4%BB%B6.html" data-v-2f2cfafc><!--[-->Vue3+组件<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/Java%E5%AD%A6%E5%89%8D%E7%AB%AF/React.html" data-v-2f2cfafc><!--[-->React<!--]--></a></div><!--]--><!--]--></div><!--]--><!--]--></div><!--[--><!--]--></div></div></div><!--]--><!--[--><div class="VPFlyout VPNavBarMenuGroup" data-v-7f418b0f data-v-a7b5672a><button type="button" class="button" aria-haspopup="true" aria-expanded="false" data-v-a7b5672a><span class="text" data-v-a7b5672a><!----><span data-v-a7b5672a>软件测试</span><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="text-icon" data-v-a7b5672a><path d="M12,16c-0.3,0-0.5-0.1-0.7-0.3l-6-6c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l5.3,5.3l5.3-5.3c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-6,6C12.5,15.9,12.3,16,12,16z"></path></svg></span></button><div class="menu" data-v-a7b5672a><div class="VPMenu" data-v-a7b5672a data-v-e7ea1737><div class="items" data-v-e7ea1737><!--[--><!--[--><div class="VPMenuGroup" data-v-e7ea1737 data-v-69e747b5><!----><!--[--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/%E8%BD%AF%E4%BB%B6%E6%B5%8B%E8%AF%95/%E6%B5%8B%E8%AF%95%E5%9F%BA%E7%A1%80.html" data-v-2f2cfafc><!--[-->测试基础<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/%E8%BD%AF%E4%BB%B6%E6%B5%8B%E8%AF%95/%E5%8E%8B%E5%8A%9B%E6%B5%8B%E8%AF%95.html" data-v-2f2cfafc><!--[-->压力测试<!--]--></a></div><!--]--><!--]--></div><!--]--><!--]--></div><!--[--><!--]--></div></div></div><!--]--><!--[--><div class="VPFlyout VPNavBarMenuGroup" data-v-7f418b0f data-v-a7b5672a><button type="button" class="button" aria-haspopup="true" aria-expanded="false" data-v-a7b5672a><span class="text" data-v-a7b5672a><!----><span data-v-a7b5672a>多线程</span><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="text-icon" data-v-a7b5672a><path d="M12,16c-0.3,0-0.5-0.1-0.7-0.3l-6-6c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l5.3,5.3l5.3-5.3c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-6,6C12.5,15.9,12.3,16,12,16z"></path></svg></span></button><div class="menu" data-v-a7b5672a><div class="VPMenu" data-v-a7b5672a data-v-e7ea1737><div class="items" data-v-e7ea1737><!--[--><!--[--><div class="VPMenuGroup" data-v-e7ea1737 data-v-69e747b5><!----><!--[--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/%E5%B9%B6%E5%8F%91%20&amp;%20%E5%A4%9A%E7%BA%BF%E7%A8%8B/%E5%9F%BA%E7%A1%80%E7%AF%87.html" data-v-2f2cfafc><!--[-->基础篇<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/%E5%B9%B6%E5%8F%91%20&amp;%20%E5%A4%9A%E7%BA%BF%E7%A8%8B/%E5%B9%B6%E5%8F%91%E5%AE%8C%E5%96%84.html" data-v-2f2cfafc><!--[-->进阶篇<!--]--></a></div><!--]--><!--]--></div><!--]--><!--]--></div><!--[--><!--]--></div></div></div><!--]--><!--[--><div class="VPFlyout VPNavBarMenuGroup" data-v-7f418b0f data-v-a7b5672a><button type="button" class="button" aria-haspopup="true" aria-expanded="false" data-v-a7b5672a><span class="text" data-v-a7b5672a><!----><span data-v-a7b5672a>开发工具</span><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="text-icon" data-v-a7b5672a><path d="M12,16c-0.3,0-0.5-0.1-0.7-0.3l-6-6c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l5.3,5.3l5.3-5.3c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-6,6C12.5,15.9,12.3,16,12,16z"></path></svg></span></button><div class="menu" data-v-a7b5672a><div class="VPMenu" data-v-a7b5672a data-v-e7ea1737><div class="items" data-v-e7ea1737><!--[--><!--[--><div class="VPMenuGroup" data-v-e7ea1737 data-v-69e747b5><!----><!--[--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/IDEA/Chrome.html" data-v-2f2cfafc><!--[-->Chrome<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/IDEA/IDEA%E5%9F%BA%E7%A1%80.html" data-v-2f2cfafc><!--[-->IDEA基础<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/IDEA/IDEA%E6%8F%92%E4%BB%B6.html" data-v-2f2cfafc><!--[-->IDEA插件<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/IDEA/VS%20Code.html" data-v-2f2cfafc><!--[-->VS Code<!--]--></a></div><!--]--><!--]--></div><!--]--><!--]--></div><!--[--><!--]--></div></div></div><!--]--><!--[--><div class="VPFlyout VPNavBarMenuGroup" data-v-7f418b0f data-v-a7b5672a><button type="button" class="button" aria-haspopup="true" aria-expanded="false" data-v-a7b5672a><span class="text" data-v-a7b5672a><!----><span data-v-a7b5672a>消息中间件</span><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="text-icon" data-v-a7b5672a><path d="M12,16c-0.3,0-0.5-0.1-0.7-0.3l-6-6c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l5.3,5.3l5.3-5.3c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-6,6C12.5,15.9,12.3,16,12,16z"></path></svg></span></button><div class="menu" data-v-a7b5672a><div class="VPMenu" data-v-a7b5672a data-v-e7ea1737><div class="items" data-v-e7ea1737><!--[--><!--[--><div class="VPMenuGroup" data-v-e7ea1737 data-v-69e747b5><!----><!--[--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/%E6%B6%88%E6%81%AF%E4%B8%AD%E9%97%B4%E4%BB%B6/RabbitMQ.html" data-v-2f2cfafc><!--[-->RabbitMQ<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/%E6%B6%88%E6%81%AF%E4%B8%AD%E9%97%B4%E4%BB%B6/RocketMQ.html" data-v-2f2cfafc><!--[-->RocketMQ<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/%E6%B6%88%E6%81%AF%E4%B8%AD%E9%97%B4%E4%BB%B6/Kafka.html" data-v-2f2cfafc><!--[-->Kafka<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/%E6%B6%88%E6%81%AF%E4%B8%AD%E9%97%B4%E4%BB%B6/Canal.html" data-v-2f2cfafc><!--[-->Canal<!--]--></a></div><!--]--><!--]--></div><!--]--><!--]--></div><!--[--><!--]--></div></div></div><!--]--><!--]--></nav><!----><div class="VPNavBarAppearance appearance" data-v-0937f67c data-v-f6a63727><button class="VPSwitch VPSwitchAppearance" type="button" role="switch" title="toggle dark mode" aria-checked="false" data-v-f6a63727 data-v-82b282f1 data-v-f3c41672><span class="check" data-v-f3c41672><span class="icon" data-v-f3c41672><!--[--><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="sun" data-v-82b282f1><path d="M12,18c-3.3,0-6-2.7-6-6s2.7-6,6-6s6,2.7,6,6S15.3,18,12,18zM12,8c-2.2,0-4,1.8-4,4c0,2.2,1.8,4,4,4c2.2,0,4-1.8,4-4C16,9.8,14.2,8,12,8z"></path><path d="M12,4c-0.6,0-1-0.4-1-1V1c0-0.6,0.4-1,1-1s1,0.4,1,1v2C13,3.6,12.6,4,12,4z"></path><path d="M12,24c-0.6,0-1-0.4-1-1v-2c0-0.6,0.4-1,1-1s1,0.4,1,1v2C13,23.6,12.6,24,12,24z"></path><path d="M5.6,6.6c-0.3,0-0.5-0.1-0.7-0.3L3.5,4.9c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l1.4,1.4c0.4,0.4,0.4,1,0,1.4C6.2,6.5,5.9,6.6,5.6,6.6z"></path><path d="M19.8,20.8c-0.3,0-0.5-0.1-0.7-0.3l-1.4-1.4c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l1.4,1.4c0.4,0.4,0.4,1,0,1.4C20.3,20.7,20,20.8,19.8,20.8z"></path><path d="M3,13H1c-0.6,0-1-0.4-1-1s0.4-1,1-1h2c0.6,0,1,0.4,1,1S3.6,13,3,13z"></path><path d="M23,13h-2c-0.6,0-1-0.4-1-1s0.4-1,1-1h2c0.6,0,1,0.4,1,1S23.6,13,23,13z"></path><path d="M4.2,20.8c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l1.4-1.4c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-1.4,1.4C4.7,20.7,4.5,20.8,4.2,20.8z"></path><path d="M18.4,6.6c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l1.4-1.4c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-1.4,1.4C18.9,6.5,18.6,6.6,18.4,6.6z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="moon" data-v-82b282f1><path d="M12.1,22c-0.3,0-0.6,0-0.9,0c-5.5-0.5-9.5-5.4-9-10.9c0.4-4.8,4.2-8.6,9-9c0.4,0,0.8,0.2,1,0.5c0.2,0.3,0.2,0.8-0.1,1.1c-2,2.7-1.4,6.4,1.3,8.4c2.1,1.6,5,1.6,7.1,0c0.3-0.2,0.7-0.3,1.1-0.1c0.3,0.2,0.5,0.6,0.5,1c-0.2,2.7-1.5,5.1-3.6,6.8C16.6,21.2,14.4,22,12.1,22zM9.3,4.4c-2.9,1-5,3.6-5.2,6.8c-0.4,4.4,2.8,8.3,7.2,8.7c2.1,0.2,4.2-0.4,5.8-1.8c1.1-0.9,1.9-2.1,2.4-3.4c-2.5,0.9-5.3,0.5-7.5-1.1C9.2,11.4,8.1,7.7,9.3,4.4z"></path></svg><!--]--></span></span></button></div><div class="VPSocialLinks VPNavBarSocialLinks social-links" data-v-0937f67c data-v-0394ad82 data-v-7bc22406><!--[--><a class="VPSocialLink no-icon" href="https://github.com/renshuo123/renshuo123.github.io" aria-label="github" target="_blank" rel="noopener" data-v-7bc22406 data-v-f80f8133><svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg></a><a class="VPSocialLink no-icon" href="#" aria-label="twitter" target="_blank" rel="noopener" data-v-7bc22406 data-v-f80f8133><svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Twitter</title><path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/></svg></a><a class="VPSocialLink no-icon" href="https://github.com/" aria-label target="_blank" rel="noopener" data-v-7bc22406 data-v-f80f8133><svg t="1676028692954" class="icon" ...</path></svg></a><!--]--></div><div class="VPFlyout VPNavBarExtra extra" data-v-0937f67c data-v-40855f84 data-v-a7b5672a><button type="button" class="button" aria-haspopup="true" aria-expanded="false" aria-label="extra navigation" data-v-a7b5672a><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="icon" data-v-a7b5672a><circle cx="12" cy="12" r="2"></circle><circle cx="19" cy="12" r="2"></circle><circle cx="5" cy="12" r="2"></circle></svg></button><div class="menu" data-v-a7b5672a><div class="VPMenu" data-v-a7b5672a data-v-e7ea1737><!----><!--[--><!--[--><!----><div class="group" data-v-40855f84><div class="item appearance" data-v-40855f84><p class="label" data-v-40855f84>Appearance</p><div class="appearance-action" data-v-40855f84><button class="VPSwitch VPSwitchAppearance" type="button" role="switch" title="toggle dark mode" aria-checked="false" data-v-40855f84 data-v-82b282f1 data-v-f3c41672><span class="check" data-v-f3c41672><span class="icon" data-v-f3c41672><!--[--><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="sun" data-v-82b282f1><path d="M12,18c-3.3,0-6-2.7-6-6s2.7-6,6-6s6,2.7,6,6S15.3,18,12,18zM12,8c-2.2,0-4,1.8-4,4c0,2.2,1.8,4,4,4c2.2,0,4-1.8,4-4C16,9.8,14.2,8,12,8z"></path><path d="M12,4c-0.6,0-1-0.4-1-1V1c0-0.6,0.4-1,1-1s1,0.4,1,1v2C13,3.6,12.6,4,12,4z"></path><path d="M12,24c-0.6,0-1-0.4-1-1v-2c0-0.6,0.4-1,1-1s1,0.4,1,1v2C13,23.6,12.6,24,12,24z"></path><path d="M5.6,6.6c-0.3,0-0.5-0.1-0.7-0.3L3.5,4.9c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l1.4,1.4c0.4,0.4,0.4,1,0,1.4C6.2,6.5,5.9,6.6,5.6,6.6z"></path><path d="M19.8,20.8c-0.3,0-0.5-0.1-0.7-0.3l-1.4-1.4c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l1.4,1.4c0.4,0.4,0.4,1,0,1.4C20.3,20.7,20,20.8,19.8,20.8z"></path><path d="M3,13H1c-0.6,0-1-0.4-1-1s0.4-1,1-1h2c0.6,0,1,0.4,1,1S3.6,13,3,13z"></path><path d="M23,13h-2c-0.6,0-1-0.4-1-1s0.4-1,1-1h2c0.6,0,1,0.4,1,1S23.6,13,23,13z"></path><path d="M4.2,20.8c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l1.4-1.4c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-1.4,1.4C4.7,20.7,4.5,20.8,4.2,20.8z"></path><path d="M18.4,6.6c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l1.4-1.4c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-1.4,1.4C18.9,6.5,18.6,6.6,18.4,6.6z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="moon" data-v-82b282f1><path d="M12.1,22c-0.3,0-0.6,0-0.9,0c-5.5-0.5-9.5-5.4-9-10.9c0.4-4.8,4.2-8.6,9-9c0.4,0,0.8,0.2,1,0.5c0.2,0.3,0.2,0.8-0.1,1.1c-2,2.7-1.4,6.4,1.3,8.4c2.1,1.6,5,1.6,7.1,0c0.3-0.2,0.7-0.3,1.1-0.1c0.3,0.2,0.5,0.6,0.5,1c-0.2,2.7-1.5,5.1-3.6,6.8C16.6,21.2,14.4,22,12.1,22zM9.3,4.4c-2.9,1-5,3.6-5.2,6.8c-0.4,4.4,2.8,8.3,7.2,8.7c2.1,0.2,4.2-0.4,5.8-1.8c1.1-0.9,1.9-2.1,2.4-3.4c-2.5,0.9-5.3,0.5-7.5-1.1C9.2,11.4,8.1,7.7,9.3,4.4z"></path></svg><!--]--></span></span></button></div></div></div><div class="group" data-v-40855f84><div class="item social-links" data-v-40855f84><div class="VPSocialLinks social-links-list" data-v-40855f84 data-v-7bc22406><!--[--><a class="VPSocialLink no-icon" href="https://github.com/renshuo123/renshuo123.github.io" aria-label="github" target="_blank" rel="noopener" data-v-7bc22406 data-v-f80f8133><svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg></a><a class="VPSocialLink no-icon" href="#" aria-label="twitter" target="_blank" rel="noopener" data-v-7bc22406 data-v-f80f8133><svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Twitter</title><path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/></svg></a><a class="VPSocialLink no-icon" href="https://github.com/" aria-label target="_blank" rel="noopener" data-v-7bc22406 data-v-f80f8133><svg t="1676028692954" class="icon" ...</path></svg></a><!--]--></div></div></div><!--]--><!--]--></div></div></div><!--[--><!--]--><button type="button" class="VPNavBarHamburger hamburger" aria-label="mobile navigation" aria-expanded="false" aria-controls="VPNavScreen" data-v-0937f67c data-v-e5dd9c1c><span class="container" data-v-e5dd9c1c><span class="top" data-v-e5dd9c1c></span><span class="middle" data-v-e5dd9c1c></span><span class="bottom" data-v-e5dd9c1c></span></span></button></div></div></div></div><!----></header><div class="VPLocalNav reached-top" data-v-255ec12d data-v-5cfd5582><button class="menu" aria-expanded="false" aria-controls="VPSidebarNav" data-v-5cfd5582><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="menu-icon" data-v-5cfd5582><path d="M17,11H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S17.6,11,17,11z"></path><path d="M21,7H3C2.4,7,2,6.6,2,6s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,7,21,7z"></path><path d="M21,15H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,15,21,15z"></path><path d="M17,19H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S17.6,19,17,19z"></path></svg><span class="menu-text" data-v-5cfd5582>Menu</span></button><div class="VPLocalNavOutlineDropdown" style="--vp-vh:0px;" data-v-5cfd5582 data-v-18201f51><button data-v-18201f51>Return to top</button><!----></div></div><aside class="VPSidebar" data-v-255ec12d data-v-845b8fc6><div class="curtain" data-v-845b8fc6></div><nav class="nav" id="VPSidebarNav" aria-labelledby="sidebar-aria-label" tabindex="-1" data-v-845b8fc6><span class="visually-hidden" id="sidebar-aria-label" data-v-845b8fc6> Sidebar Navigation </span><!--[--><!--]--><!--[--><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>Java</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Java/Java%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Java基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Java/Java%E6%96%B0%E7%89%B9%E6%80%A7.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Java新特性</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Java/Java%E8%BF%9B%E9%98%B6.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Java进阶</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Java/Java%E9%9B%86%E5%90%88.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Java集合</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Java/Java%E9%AB%98%E7%BA%A7.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Java高级</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>Linux</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Linux/Linux%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Linux基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Linux/Linux%E8%BF%9B%E9%98%B6.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Linux新特性</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Linux/Shell.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Shell脚本</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Linux/%E5%AE%9E%E7%94%A8%E8%84%9A%E6%9C%AC.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>实用脚本</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Linux/%E8%BD%AF%E4%BB%B6%E9%83%A8%E7%BD%B2.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>软件部署</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>Nginx</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Nginx/%E5%9F%BA%E7%A1%80%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>基础篇</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Nginx/%E8%BF%9B%E9%98%B6%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>进阶篇</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Nginx/%E5%AE%9E%E6%88%98%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>实战篇</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Nginx/%E9%9D%A2%E8%AF%95%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>面试篇</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>SSM</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/SSM/Maven.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Maven</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/SSM/Spring.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Spring</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/SSM/SpringMVC.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>SpringMVC</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/SSM/SpringBatch.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>SpringBatch</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>SpringBoot</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/3%E3%80%81SpringBoot/%E5%9F%BA%E7%A1%80%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>基础篇</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/3%E3%80%81SpringBoot/%E5%BA%94%E7%94%A8%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>应用篇</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/3%E3%80%81SpringBoot/%E6%96%B0%E7%89%B9%E6%80%A7.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>新特性</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/3%E3%80%81SpringBoot/%E8%BF%90%E7%BB%B4&amp;%E5%8E%9F%E7%90%86.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>运维&原理</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>SpringCloud</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/4%E3%80%81%E5%BE%AE%E6%9C%8D%E5%8A%A1/%E8%BF%9B%E9%98%B6.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>SpringCloud</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/4%E3%80%81%E5%BE%AE%E6%9C%8D%E5%8A%A1/%E5%BF%85%E5%A4%87/Sentinel.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Sentinel</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>SpringSecurity</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/4%E3%80%81%E5%BE%AE%E6%9C%8D%E5%8A%A1/SpringSecurity/%E5%9F%BA%E7%A1%80%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>SpringSecurity基础篇</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/4%E3%80%81%E5%BE%AE%E6%9C%8D%E5%8A%A1/SpringSecurity/%E8%BF%9B%E9%98%B6%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>SpringSecurity进阶篇</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/4%E3%80%81%E5%BE%AE%E6%9C%8D%E5%8A%A1/SpringSecurity/%E9%AB%98%E7%BA%A7%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>SpringSecurity高级篇</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>Mybatis & MybatisPlus</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Mybatis&amp;MybatisPlus/Mybatis.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Mybatis</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Mybatis&amp;MybatisPlus/MybatisPlus.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>MybatisPlus</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Mybatis&amp;MybatisPlus/JPA.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>JPA</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>Git & ChatGPT</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/5%E3%80%81%E8%BF%90%E7%BB%B4/Git.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Git</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/5%E3%80%81%E8%BF%90%E7%BB%B4/Github.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Github</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/5%E3%80%81%E8%BF%90%E7%BB%B4/ChatGPT.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>ChatGPT</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/5%E3%80%81%E8%BF%90%E7%BB%B4/Jenkins.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Jenkins</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/5%E3%80%81%E8%BF%90%E7%BB%B4/Netty.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Netty</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>数据库</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>MySQL</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL/MySQL%E6%A0%B8%E5%BF%83/%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>MySQL基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL/MySQL%E6%A0%B8%E5%BF%83/%E8%BF%9B%E9%98%B6.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>MySQL进阶</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL/MySQL%E6%A0%B8%E5%BF%83/%E4%BC%98%E5%8C%96.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>MySQL优化</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL/MySQL%E6%A0%B8%E5%BF%83/%E8%AE%BE%E8%AE%A1.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>MySQL设计</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL/MySQL%E6%A0%B8%E5%BF%83/%E8%BF%90%E7%BB%B4.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>MySQL运维</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL/%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>分库分表</p><!--]--></a><!----></div><!----></div><!--]--></div></section><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>Redis</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/Redis/Redis%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Redis基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/Redis/Redis%E4%BC%98%E5%8C%96.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Redis优化</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/Redis/Redis%E5%8E%9F%E7%90%86.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Redis原理</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/Redis/Redis%E9%AB%98%E7%BA%A7.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Redis高级</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/Redis/Redis%E5%AE%9E%E6%88%98.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Redis实战</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/Redis/%E6%9C%AC%E5%9C%B0%E7%BC%93%E5%AD%98.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>本地缓存</p><!--]--></a><!----></div><!----></div><!--]--></div></section><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>MongoDB</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/MongoDB/%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>MongoDB基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/MongoDB/%E6%95%B4%E5%90%88.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>MongoDB进阶</p><!--]--></a><!----></div><!----></div><!--]--></div></section><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>ElasticSearch</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/ElasticSearch/1%E3%80%81ES%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>ES基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/ElasticSearch/3%E3%80%81ES%E9%AB%98%E7%BA%A7.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>ES高级</p><!--]--></a><!----></div><!----></div><!--]--></div></section><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/influxdb.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>InfluxDB</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/Neo4j.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Neo4j</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible has-active" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>高并发 & 秒杀 & 分布式</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E4%B8%89%E9%AB%98/%E5%88%86%E5%B8%83%E5%BC%8F.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>分布式理论</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/4%E3%80%81%E5%BE%AE%E6%9C%8D%E5%8A%A1/%E5%BF%85%E5%A4%87/%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>分布式锁</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E4%B8%89%E9%AB%98/%E7%A7%92%E6%9D%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>秒杀</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E4%B8%89%E9%AB%98/%E9%AB%98%E5%8F%AF%E7%94%A8.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>高可用</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E4%B8%89%E9%AB%98/%E9%AB%98%E5%B9%B6%E5%8F%91.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>高并发</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>云原生</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E4%BA%91%E5%8E%9F%E7%94%9F/Docker.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Docker</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E4%BA%91%E5%8E%9F%E7%94%9F/K8S.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>K8S</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>可视化 & 监控</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E5%8F%AF%E8%A7%86%E5%8C%96%20&amp;%20%E7%9B%91%E6%8E%A7/%E7%9B%91%E6%8E%A7%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>监控基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E5%8F%AF%E8%A7%86%E5%8C%96%20&amp;%20%E7%9B%91%E6%8E%A7/%E7%9B%91%E6%8E%A7%E8%BF%9B%E9%98%B6.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>监控进阶</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E5%8F%AF%E8%A7%86%E5%8C%96%20&amp;%20%E7%9B%91%E6%8E%A7/%E5%8F%AF%E8%A7%86%E5%8C%96%E5%A4%A7%E5%B1%8F.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>可视化大屏</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E5%8F%AF%E8%A7%86%E5%8C%96%20&amp;%20%E7%9B%91%E6%8E%A7/Zabbix.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Zabbix</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>学前端</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>HTML+CSS</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/1%E3%80%81HTML+CSS/HTML%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>HTML基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/1%E3%80%81HTML+CSS/CSS%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>CSS基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/1%E3%80%81HTML+CSS/%E7%BD%91%E9%A1%B5%E8%BF%9B%E9%98%B6.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>网页进阶</p><!--]--></a><!----></div><!----></div><!--]--></div></section><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>JS+TS</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/2%E3%80%81JS+TS/JS%20%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>JS基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/2%E3%80%81JS+TS/JS%20%E8%BF%9B%E9%98%B6.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>JS进阶</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/2%E3%80%81JS+TS/ES6%20%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>ES6基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/2%E3%80%81JS+TS/ES6%20%E8%BF%9B%E9%98%B6.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>ES6进阶</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/2%E3%80%81JS+TS/TypeScript.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>TS基础</p><!--]--></a><!----></div><!----></div><!--]--></div></section><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>NodeJS</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/4%E3%80%81Node/%E5%9F%BA%E7%A1%80%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Node基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/4%E3%80%81Node/%E8%BF%9B%E9%98%B6%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Node进阶</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/4%E3%80%81Node/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>项目实战</p><!--]--></a><!----></div><!----></div><!--]--></div></section><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>Vue</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/3%E3%80%81Vue/Vue3/Vue3%E8%BF%9B%E9%98%B6.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Vue3进阶</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/3%E3%80%81Vue/Vue3/Vue3%E9%AB%98%E7%BA%A7.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Vue3高级</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/3%E3%80%81Vue/Vue3/Vue3%E6%96%B0%E8%AF%AD%E6%B3%95.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Vue3新语法</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/3%E3%80%81Vue/Vue2/Vue2%E9%A1%B9%E7%9B%AE.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>项目实战</p><!--]--></a><!----></div><!----></div><!--]--></div></section><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>小程序</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/5%E3%80%81%E5%B0%8F%E7%A8%8B%E5%BA%8F/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>小程序基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/5%E3%80%81%E5%B0%8F%E7%A8%8B%E5%BA%8F/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E4%BC%98%E5%8C%96.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>小程序优化</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/5%E3%80%81%E5%B0%8F%E7%A8%8B%E5%BA%8F/uniapp.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>uniapp</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/5%E3%80%81%E5%B0%8F%E7%A8%8B%E5%BA%8F/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E9%A1%B9%E7%9B%AE.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>项目实战</p><!--]--></a><!----></div><!----></div><!--]--></div></section><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>计算机基础</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%9F%BA%E7%A1%80%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>数据结构</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>操作系统</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/%E5%9F%BA%E7%A1%80%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>设计模式</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/%E7%BD%91%E7%BB%9C%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>计算机网络</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/UML.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>UML</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E7%AE%97%E6%B3%95/LeetCode.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>LeetCode</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>项目实战</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>云尚办公</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E4%BA%91%E5%B0%9A%E5%8A%9E%E5%85%AC/%E5%9F%BA%E7%A1%80%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>基础篇</p><!--]--></a><!----></div><!----></div><!--]--></div></section><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>小兔鲜</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E5%B0%8F%E5%85%94%E9%B2%9C/%E5%9F%BA%E7%A1%80%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>基础篇</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E5%B0%8F%E5%85%94%E9%B2%9C/%E8%BF%9B%E9%98%B6%E7%AF%871.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>进阶篇1</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E5%B0%8F%E5%85%94%E9%B2%9C/%E8%BF%9B%E9%98%B6%E7%AF%872.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>进阶篇2</p><!--]--></a><!----></div><!----></div><!--]--></div></section><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>地图</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E7%99%BE%E5%BA%A6%E5%9C%B0%E5%9B%BE/%E5%9F%BA%E7%A1%80%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>基础篇</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E7%99%BE%E5%BA%A6%E5%9C%B0%E5%9B%BE/%E8%BF%9B%E9%98%B6%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>进阶篇</p><!--]--></a><!----></div><!----></div><!--]--></div></section><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>苍穹外卖</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E8%8B%8D%E7%A9%B9%E5%A4%96%E5%8D%96/%E8%BF%9B%E9%98%B6%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>进阶篇</p><!--]--></a><!----></div><!----></div><!--]--></div></section><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>黑马头条</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E9%BB%91%E9%A9%AC%E5%A4%B4%E6%9D%A1/%E5%9F%BA%E7%A1%80%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>基础篇</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E9%BB%91%E9%A9%AC%E5%A4%B4%E6%9D%A1/%E8%BF%9B%E9%98%B6%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>进阶篇</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E9%BB%91%E9%A9%AC%E5%A4%B4%E6%9D%A1/%E8%BF%9B%E9%98%B6%E7%AF%872.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>进阶篇2</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E9%BB%91%E9%A9%AC%E5%A4%B4%E6%9D%A1/%E9%AB%98%E7%BA%A7%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>高级篇</p><!--]--></a><!----></div><!----></div><!--]--></div></section><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E6%94%AF%E4%BB%98.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>支付</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E9%A1%B9%E7%9B%AE%E6%8E%A8%E8%8D%90.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>项目推荐</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0" data-v-845b8fc6 data-v-9b797284><!----><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/team.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>团队成员</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><!--]--><!--[--><!--]--></nav></aside><div class="VPContent has-sidebar" id="VPContent" data-v-255ec12d data-v-669faec9><div class="VPDoc has-sidebar has-aside" data-v-669faec9 data-v-6b87e69f><!--[--><!--]--><div class="container" data-v-6b87e69f><div class="aside" data-v-6b87e69f><div class="aside-curtain" data-v-6b87e69f></div><div class="aside-container" data-v-6b87e69f><div class="aside-content" data-v-6b87e69f><div class="VPDocAside" data-v-6b87e69f data-v-3f215769><!--[--><!--]--><!--[--><!--]--><div class="VPDocAsideOutline" data-v-3f215769 data-v-ff0f39c8><div class="content" data-v-ff0f39c8><div class="outline-marker" data-v-ff0f39c8></div><div class="outline-title" data-v-ff0f39c8>On this page</div><nav aria-labelledby="doc-outline-aria-label" data-v-ff0f39c8><span class="visually-hidden" id="doc-outline-aria-label" data-v-ff0f39c8> Table of Contents for current page </span><ul class="root" data-v-ff0f39c8 data-v-d0ee3533><!--[--><!--]--></ul></nav></div></div><!--[--><!--]--><div class="spacer" data-v-3f215769></div><!--[--><!--]--><!----><!--[--><!--]--><!--[--><!--]--></div></div></div></div><div class="content" data-v-6b87e69f><div class="content-container" data-v-6b87e69f><!--[--><!--]--><!----><main class="main" data-v-6b87e69f><div style="position:relative;" class="vp-doc _notebook_%E4%B8%89%E9%AB%98_%E7%A7%92%E6%9D%80" data-v-6b87e69f><div><h1 id="秒杀思路" tabindex="-1">秒杀思路 <a class="header-anchor" href="#秒杀思路" aria-label="Permalink to &quot;秒杀思路&quot;">​</a></h1><h2 id="前言" tabindex="-1">前言 <a class="header-anchor" href="#前言" aria-label="Permalink to &quot;前言&quot;">​</a></h2><p>最近心血来潮，想起前段时间公司举办的线下秒杀活动不理想，想研究一下秒杀系统的优化。当时活动现场有 200+ 会员，由于我们先前没有经验，各种原因导致用户在秒杀的时候 APP 页面白屏、卡死。业务部门想把手机甩我们开发脸上......当时我刚毕业也刚入职不久，不敢发表意见。现在逐渐膨胀，是时候重新设计一套秒杀系统了......</p><h2 id="问题分析" tabindex="-1">问题分析 <a class="header-anchor" href="#问题分析" aria-label="Permalink to &quot;问题分析&quot;">​</a></h2><p>有经验的同学看到 200+ 会员都出现白屏、卡死，可能会觉得公司技术太 low 。其实不然，公司系统架构还是很好的，大佬搭建了一套 SpringCloud 组件，都是比较新的版本。这次秒杀活动失利的确是之前没有这样的经验，很多代码考虑没到位，访问数据库的次数太多。虽然会员数是 200 ，但是会员从进入秒杀页面，点击秒杀商品，再到秒杀下单，这中间夸了几个微服务，对于数据库的访问远远不止 200 。</p><p>对代码分析之后，我们公司秒杀其实是和普通订单是同一个流程，只是加了一个秒杀 ID 字段。下单流程在一个事务里面各种校验、跨服务调用、加锁扣减库存、插入订单商品信息、物流配送信息......等。这样跨服务和多次访问数据库很明显无法满足秒杀业务瞬间流量巨大的特性。就像下面的图</p><p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.23/202206281756833.png" alt="image-20220628175623778"></p><p>上面就是我们下单的粗略流程，其实具体比这个要复杂，在每个微服务里面又调用了其他服务访问数据库。。。因为一个用户至少就是一个线程，当用户量过大线程数就很多，服务资源是有限的。当资源不够用的时候，后来的用户请求就会等待服务器释放资源处理，用户就会觉得卡。用户一旦觉得卡，就很可能会回退页面刷新，或者再次点击提交订单，然后请求又过来，又访问数据库，就会更卡。一次请求每多访问一次数据库，就需要更多的时间来处理，所以请求太多最终服务器处理不过来，前端得不到响应，用户屏幕会卡在那白屏。</p><p>还有一个关键原因是秒杀商品的查询，也是走的数据库，而且由于公司业务特殊性，不同地区的会员看到的商品不同等其他业务，导致秒杀商品的查询也比较慢，系统吞吐量低，最后可能导致用户白屏，流程和上面下单的差不多。说到底就是要提高系统吞吐量，让服务器尽快释放资源。</p><p>针对上述问题：在应对秒杀系统这样一瞬间的巨大流量，现在系统架构存在的核心问题：</p><ul><li>没有遵循服务单一原则，把秒杀功能做在订单服务里面，万一秒杀系统压力过大还会影响到正常的订单业务</li><li>和普通订单一样巨大流量蜂拥而至加锁扣减库存，导致很多无效请求占用资源</li><li>秒杀订单链接没有加密，给专业团队可趁之机</li><li>大量操作直接操作数据库，频繁磁盘 IO，系统吞吐量很低，甚至有可能数据库挂掉</li></ul><p>以上是几大核心问题，解决了这几个问题，基本上就可以实现较好的秒杀系统了。下面全面分析、解决秒杀系统问题：</p><h2 id="服务单一职责" tabindex="-1">服务单一职责 <a class="header-anchor" href="#服务单一职责" aria-label="Permalink to &quot;服务单一职责&quot;">​</a></h2><blockquote><p>我们都知道秒杀的特性，瞬间流量巨大。如果将秒杀功能做在订单服务里面，万一秒杀占用的资源过多，或者秒杀功能直接把服务搞挂，正常订单业务也会受影响。所以秒杀要单独部署微服务</p></blockquote><h2 id="巨大流量处理-限流" tabindex="-1">巨大流量处理(限流) <a class="header-anchor" href="#巨大流量处理-限流" aria-label="Permalink to &quot;巨大流量处理(限流)&quot;">​</a></h2><p>秒杀一瞬间的巨大流量不仅仅有广大用户正常请求，还有用户不必要的频繁点击、恶意用户、恶意攻击等。如果不做好处理很有可能请求还没到库存扣减那里，微服务集群就顶不住了。对于巨大的流量，采取适当的限流措施是很有必要的。常用限流方式有下面几种：</p><h3 id="前端限流" tabindex="-1">前端限流 <a class="header-anchor" href="#前端限流" aria-label="Permalink to &quot;前端限流&quot;">​</a></h3><p>我们要在巨大流量的基础上去尽可能减少一些不必要的流量，活动未开始的时候前端按钮就置灰，不让点击，活动开始之后限制用户点击按钮的频率。这样可以去除正常用户的大量不必要请求。</p><h3 id="nginx-限流" tabindex="-1">Nginx 限流 <a class="header-anchor" href="#nginx-限流" aria-label="Permalink to &quot;Nginx 限流&quot;">​</a></h3><p>前端限流只能防止正常用户，但是有些恶意用户有点开发知识，通过网页获取 URL 来模拟请求，这里可以通过 Nginx 对同一 IP 做出每秒访问次数限制。</p><div class="language-apl"><button title="Copy Code" class="copy"></button><span class="lang">apl</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">limit_req_zone $binary_remote_addr zone</span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;">one:</span><span style="color:#F78C6C;">10m</span><span style="color:#A6ACCD;"> rate</span><span style="color:#89DDFF;">=</span><span style="color:#F78C6C;">20r</span><span style="color:#89DDFF;">/</span><span style="color:#A6ACCD;">s</span><span style="color:#89DDFF;">;</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">#</span><span style="color:#A6ACCD;">限制同一IP 允许访问</span><span style="color:#F78C6C;">20</span><span style="color:#A6ACCD;">次</span><span style="color:#89DDFF;">/</span><span style="color:#A6ACCD;">S</span></span></code></pre></div><h3 id="网关限流" tabindex="-1">网关限流 <a class="header-anchor" href="#网关限流" aria-label="Permalink to &quot;网关限流&quot;">​</a></h3><p>根据现有的网关集群、秒杀服务集群，估算集群能够支撑的最大请求，然后限制流量。网关限流以 GateWay 为例，可以对 IP、用户、接口限流，这里可以选择对秒杀接口限流。常用的网关限流算法有，漏桶算法 和 令牌桶算法，我们选择使用令牌桶算法限流，因为这个算法可允许突发的瞬间流量处理，GateWay 内置了一个过滤器工厂配置 RequestRateLimiterGatewayFilterFactory 使用它即可，它需要依赖 Redis</p><div class="language-scala"><button title="Copy Code" class="copy"></button><span class="lang">scala</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#89DDFF;">&lt;</span><span style="color:#F07178;">dependency</span><span style="color:#89DDFF;">&gt;</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">&lt;</span><span style="color:#F07178;">groupId</span><span style="color:#89DDFF;">&gt;</span><span style="color:#A6ACCD;">org.springframework.boot</span><span style="color:#89DDFF;">&lt;/</span><span style="color:#F07178;">groupId</span><span style="color:#89DDFF;">&gt;</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">&lt;</span><span style="color:#F07178;">artifatId</span><span style="color:#89DDFF;">&gt;</span><span style="color:#A6ACCD;">spring</span><span style="color:#89DDFF;">-</span><span style="color:#A6ACCD;">boot</span><span style="color:#89DDFF;">-</span><span style="color:#A6ACCD;">starter</span><span style="color:#89DDFF;">-</span><span style="color:#A6ACCD;">data</span><span style="color:#89DDFF;">-</span><span style="color:#A6ACCD;">redis</span><span style="color:#89DDFF;">-</span><span style="color:#A6ACCD;">reactive</span><span style="color:#89DDFF;">&lt;/</span><span style="color:#F07178;">artifactId</span><span style="color:#89DDFF;">&gt;</span></span>
<span class="line"><span style="color:#89DDFF;">&lt;/</span><span style="color:#F07178;">dependency</span><span style="color:#89DDFF;">&gt;</span></span></code></pre></div><p>配置：</p><div class="language-yml"><button title="Copy Code" class="copy"></button><span class="lang">yml</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#F07178;">server</span><span style="color:#89DDFF;">:</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#F07178;">port</span><span style="color:#89DDFF;">:</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">40000</span></span>
<span class="line"><span style="color:#F07178;">spring</span><span style="color:#89DDFF;">:</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#F07178;">cloud</span><span style="color:#89DDFF;">:</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#F07178;">gateway</span><span style="color:#89DDFF;">:</span></span>
<span class="line"><span style="color:#A6ACCD;">      </span><span style="color:#F07178;">routes</span><span style="color:#89DDFF;">:</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;">-</span><span style="color:#A6ACCD;"> </span><span style="color:#F07178;">id</span><span style="color:#89DDFF;">:</span><span style="color:#A6ACCD;"> </span><span style="color:#C3E88D;">sec_kill_route</span></span>
<span class="line"><span style="color:#A6ACCD;">          </span><span style="color:#F07178;">uri</span><span style="color:#89DDFF;">:</span><span style="color:#A6ACCD;"> </span><span style="color:#C3E88D;">lb://hosjoy-b2b-seckill</span></span>
<span class="line"><span style="color:#A6ACCD;">          </span><span style="color:#F07178;">predicates</span><span style="color:#89DDFF;">:</span></span>
<span class="line"><span style="color:#A6ACCD;">            </span><span style="color:#89DDFF;">-</span><span style="color:#A6ACCD;"> </span><span style="color:#C3E88D;">Path=/seckill/**</span></span>
<span class="line"><span style="color:#A6ACCD;">          </span><span style="color:#F07178;">filters</span><span style="color:#89DDFF;">:</span></span>
<span class="line"><span style="color:#A6ACCD;">            </span><span style="color:#89DDFF;">-</span><span style="color:#A6ACCD;"> </span><span style="color:#F07178;">name</span><span style="color:#89DDFF;">:</span><span style="color:#A6ACCD;"> </span><span style="color:#C3E88D;">RequestRateLimiter</span></span>
<span class="line"><span style="color:#A6ACCD;">              </span><span style="color:#F07178;">args</span><span style="color:#89DDFF;">:</span></span>
<span class="line"><span style="color:#A6ACCD;">                </span><span style="color:#F07178;">key-resolver</span><span style="color:#89DDFF;">:</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&#39;</span><span style="color:#C3E88D;">#{@apiKeyResolver}</span><span style="color:#89DDFF;">&#39;</span><span style="color:#A6ACCD;">  </span><span style="color:#676E95;font-style:italic;">#从Spring容器中获取限流的Bean</span></span>
<span class="line"><span style="color:#A6ACCD;">                </span><span style="color:#F07178;">redis-rate-limiter.replenishRate</span><span style="color:#89DDFF;">:</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">100</span><span style="color:#A6ACCD;"> </span><span style="color:#676E95;font-style:italic;">#令牌填充速率（每秒处理的请求）</span></span>
<span class="line"><span style="color:#A6ACCD;">                </span><span style="color:#F07178;">redis-rate-limiter.burstCapacity</span><span style="color:#89DDFF;">:</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">3000</span><span style="color:#A6ACCD;"> </span><span style="color:#676E95;font-style:italic;">#令牌总容量（1秒内能允许的最大请求数）</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#F07178;">application</span><span style="color:#89DDFF;">:</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#F07178;">name</span><span style="color:#89DDFF;">:</span><span style="color:#A6ACCD;"> </span><span style="color:#C3E88D;">hosjoy-b2b-gateway</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#F07178;">redis</span><span style="color:#89DDFF;">:</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#F07178;">host</span><span style="color:#89DDFF;">:</span><span style="color:#A6ACCD;"> </span><span style="color:#C3E88D;">localhost</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#F07178;">port</span><span style="color:#89DDFF;">:</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">6379</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#F07178;">database</span><span style="color:#89DDFF;">:</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">0</span></span></code></pre></div><p>代码：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">Bean</span></span>
<span class="line"><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">KeyResolver</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">apiKeyResolver</span><span style="color:#89DDFF;">()</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> exchange </span><span style="color:#C792EA;">-&gt;</span><span style="color:#A6ACCD;"> Mono</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">just</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">exchange</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">getRequest</span><span style="color:#89DDFF;">().</span><span style="color:#82AAFF;">getPath</span><span style="color:#89DDFF;">().</span><span style="color:#82AAFF;">value</span><span style="color:#89DDFF;">());</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><p>如果公司有使用阿里的 Sentinel 组件，这里也可以在网关层使用 Sentinel 做限流，功能强大，方便监控、熔断、降级，使用非常方便！</p><h3 id="集群实例扩充" tabindex="-1">集群实例扩充 <a class="header-anchor" href="#集群实例扩充" aria-label="Permalink to &quot;集群实例扩充&quot;">​</a></h3><p>没有什么流量是服务实例个数解决不了的，如果有，那么就继续加服务实例......这样通过增加服务实例来对应大流量也可以变相的达到限流效果 。</p><h2 id="应对恶意请求" tabindex="-1">应对恶意请求 <a class="header-anchor" href="#应对恶意请求" aria-label="Permalink to &quot;应对恶意请求&quot;">​</a></h2><p>相信大家都有所耳闻，有些 “专业团队” ，专门通过代别人抢茅台等商品牟利。对于这种团队，他们不仅有多 IP ，甚至还有可能有多账户！就是通过各种渠道低价购买正常用户的账号来逃避风控系统。对于这样的 “专业团队”，单纯的限流不能完全解决这个问题，你想一下，有可能发生这种情况，这些恶意的请求被处理了，抢到了商品，但是广大用户没有抢到，这个问题就很严重。对于这种情况，我们可以采取两种方案:</p><h3 id="秒杀链接加密" tabindex="-1"><strong>秒杀链接加密</strong> <a class="header-anchor" href="#秒杀链接加密" aria-label="Permalink to &quot;**秒杀链接加密**&quot;">​</a></h3><p>在秒杀控制器的传参中，我们一般会接受场次 ID、商品 ID，可以再多加一个和商品匹配的密码参数，只有密码正确才能继续流程，否则记录密码错误次数自增。这个密码是在秒杀场次和商品上架的时候随机生成，就连开发这个功能的人都不知道！</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">PostMapping</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">/sec-kill</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">)</span></span>
<span class="line"><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">void</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">secKill</span><span style="color:#89DDFF;">(@</span><span style="color:#C792EA;">RequestParam</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">secId</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">Long</span><span style="color:#A6ACCD;"> secId</span><span style="color:#89DDFF;">,</span></span>
<span class="line"><span style="color:#A6ACCD;">                    </span><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">RequestParam</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">productId</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">Long</span><span style="color:#A6ACCD;"> productId</span><span style="color:#89DDFF;">,</span></span>
<span class="line"><span style="color:#A6ACCD;">                    </span><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">RequestParam</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">password</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">String</span><span style="color:#A6ACCD;"> password</span><span style="color:#89DDFF;">){</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">SecProductResponse</span><span style="color:#A6ACCD;"> secProduct </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">SecProductResponse</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> redisTemplate</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">opsForValue</span><span style="color:#89DDFF;">()</span></span>
<span class="line"><span style="color:#A6ACCD;">                                    </span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">get</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">secId:productId</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span><span style="color:#676E95;font-style:italic;">//场次商品信息</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">secProduct</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">getPassword</span><span style="color:#89DDFF;">().</span><span style="color:#82AAFF;">equals</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">password</span><span style="color:#89DDFF;">))</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span><span style="color:#A6ACCD;"> </span><span style="color:#676E95;font-style:italic;">//校验秒杀商品密码</span></span>
<span class="line"><span style="color:#89DDFF;">        </span><span style="color:#676E95;font-style:italic;">//...</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">}</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">else</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">        stringRedisTemplate</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">opsForValue</span><span style="color:#89DDFF;">().</span><span style="color:#82AAFF;">increment</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">black:secId:productId:userId</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><h3 id="黑名单过滤" tabindex="-1">黑名单过滤 <a class="header-anchor" href="#黑名单过滤" aria-label="Permalink to &quot;黑名单过滤&quot;">​</a></h3><p>在网关层校验这个用户是不是黑名单，如果是，直接结束。</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#C792EA;">String</span><span style="color:#A6ACCD;"> s </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> stringRedisTemplate</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">opsForValue</span><span style="color:#89DDFF;">().</span><span style="color:#82AAFF;">get</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">secId:black:userId</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">s </span><span style="color:#89DDFF;">!=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">null</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&amp;&amp;</span><span style="color:#A6ACCD;"> Integer</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">parseInt</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">s</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&gt;=</span><span style="color:#A6ACCD;"> maxCount</span><span style="color:#89DDFF;">){</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><h2 id="防止用户重复购买" tabindex="-1">防止用户重复购买 <a class="header-anchor" href="#防止用户重复购买" aria-label="Permalink to &quot;防止用户重复购买&quot;">​</a></h2><p>一般来说秒杀活动对于同一个用户的购买是有限制的，如果已经购买过，那么这个用户就不应该继续购买。虽然前端已经做了限制，但是为了防止专业人士，这里在后端也要进行限制。我们可以使用 Redis 来实现</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#C792EA;">Boolean</span><span style="color:#A6ACCD;"> flag </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> stringRedisTemplate</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">opsForValue</span><span style="color:#89DDFF;">()</span></span>
<span class="line"><span style="color:#A6ACCD;">               </span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">setIfAbsent</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">secId:productId:userId</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> time</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> TimeUnit</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">SECONDS</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">flag</span><span style="color:#89DDFF;">){</span></span>
<span class="line"><span style="color:#89DDFF;">  </span><span style="color:#676E95;font-style:italic;">//...</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><h2 id="超卖控制、库存扣减" tabindex="-1">超卖控制、库存扣减 <a class="header-anchor" href="#超卖控制、库存扣减" aria-label="Permalink to &quot;超卖控制、库存扣减&quot;">​</a></h2><p>秒杀商品发生超卖是个很可怕的事情，因为秒杀商品本身就很优惠。为了吸引流量以极低的秒杀价格售卖，甚至亏本。如果一旦超卖，公司或者商家可能亏的血本无归。为了控制超卖，我们可以在扣除库存的时候加锁。但是这里必须要加分布式锁，使用本地锁不能控制住。使用分布式锁避免并发造成库存扣减超卖。但是如此一来系统吞吐量会有所下降。我们原来就是跨服务扣减库存，加分布式锁，还访问的数据库，拿大腿都能想到这个对于秒杀的请求量肯定不合适。现在我们在秒杀接口里面虽然可以优化到不跨服务访问数据库了，所以使用分布式锁也能解决这个问题。但是既然是锁，就有资源消耗。有没有不使用分布式锁的方案呢？所以我们可以换个角度考虑这个问题，秒杀商品库存一般都是有一定数量限制的，并且秒杀库存远小于商品可售卖库存。我们可以把这个秒杀库存的数量提前保存在 Redis 里面，然后用 Redis 来预先扣减库存，库存一旦扣减完，就返回秒杀结束，已抢完。如此一来，我们在这里有多少库存就会放进来多少请求，剩余的无效请求全部返回。不但防止了超卖，还做了流量限制，相对于原来的蜂拥而至排队扣减库存模式，这样吞吐量极高。我们可以采用 Redis 的分布式信号量实现，这里可以使用 Redisson 来做具体代码实现</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#C792EA;">RSemaphore</span><span style="color:#A6ACCD;"> semaphore </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> redissonClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">getSemaphore</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">secId:productId</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#C792EA;">boolean</span><span style="color:#A6ACCD;"> success</span><span style="color:#89DDFF;">=false;</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">try</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    success </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> semaphore</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">tryAcquire</span><span style="color:#89DDFF;">(</span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">,</span><span style="color:#F78C6C;">50</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;">TimeUnit</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">MILLISECONDS</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;">}</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">catch</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#C792EA;">InterruptedException</span><span style="color:#A6ACCD;"> </span><span style="color:#A6ACCD;font-style:italic;">e</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    log</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">error</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">e</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">success</span><span style="color:#89DDFF;">){</span></span>
<span class="line"><span style="color:#89DDFF;">    </span><span style="color:#676E95;font-style:italic;">//生成订单号</span></span>
<span class="line"><span style="color:#89DDFF;">    </span><span style="color:#676E95;font-style:italic;">//发送消息到MQ</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><p>其实分布式信号量也可以算是一种分布式锁，但是它的性能极高，获取一次信号量几乎是 0 - 1 ms，基本不会影响系统吞吐量。</p><h2 id="流量削峰" tabindex="-1">流量削峰 <a class="header-anchor" href="#流量削峰" aria-label="Permalink to &quot;流量削峰&quot;">​</a></h2><p>经过上面重重关卡，最后调用订单服务的请求数和秒杀商品的库存数量一样。假设 100 万人抢 400 茅台，那么就有 400 请求要调用订单服务，400 并发下单的话，由于还有一系列业务处理，并发访问数据库，其实又回到了最初的模式。在秒杀接口里面访问数据库，这样吞吐量是很低的，还有可能打挂数据库。我们应该让秒杀接口的操作全部走 Redis 。这里我们可以使用消息队列来做 <a href="http://mp.weixin.qq.com/s?__biz=MzA5ODExOTI5OA==&amp;mid=2247483667&amp;idx=1&amp;sn=7564c8b2513766225191004e1bafa928&amp;chksm=90973afea7e0b3e8ff23c3eefe44c0e6507561bfb7d8bfe2ce293f2d2610bb34d6b12735c640&amp;scene=21#wechat_redirect" target="_blank" rel="noreferrer">为什么使用消息队列？</a>使用 MQ 来削峰，平缓消费创建订单，将峰值流量散开。由于消息队列在强大并发下可能会造成消息丢失等问题，具体可参考 <a href="http://mp.weixin.qq.com/s?__biz=MzA5ODExOTI5OA==&amp;mid=2247483690&amp;idx=1&amp;sn=11b7498dd737c42da93eb3437d3792ed&amp;chksm=90973ac7a7e0b3d1a6ede4a3716a6e57031703791a652c98a3c06a2b81b03db585206c231c92&amp;scene=21#wechat_redirect" target="_blank" rel="noreferrer">RabbitMQ 可靠性、重复消费、顺序性、消息积压解决方案</a></p><h2 id="数据库分表分库" tabindex="-1">数据库分表分库 <a class="header-anchor" href="#数据库分表分库" aria-label="Permalink to &quot;数据库分表分库&quot;">​</a></h2><p>一般来说以上就能实现较好的秒杀系统效果了，如果公司数据量很大，业务很复杂。甚至 MQ 异步消费访问数据库也不能解决的话，那么就用读写分离，读库和写库分开，有效降低数据库压力。还可以去对数据库分表、分库来提升单表并发能力和磁盘 IO 读写性能。</p><p>解决以上问题，秒杀流程基本就 OK 了，其实上面的伪代码都很简单，真实实现的话，代码也不复杂，只是要合理的设计方案，该屏蔽过滤的请求就屏蔽过滤，不该访问数据库的不访问即可。下面具体看下这几个环节的流程图</p><h2 id="商品上架-库存回退" tabindex="-1">商品上架/库存回退 <a class="header-anchor" href="#商品上架-库存回退" aria-label="Permalink to &quot;商品上架/库存回退&quot;">​</a></h2><p>商品上架其实很简单，我们只需要把需要的信息存入 Redis 即可。不过不同公司有不同的业务，比如我公司的业务 B → b → c 的模式，秒杀商品、活动是有区域的，就是说一场活动可能会发生，经营区域在 A、B、C 三个市的会员店可以参与，其他区域的会员店不可以参与。所以针对这种情况，我们需要把秒杀场次信息在所有可允许的区域都要存储一份，就像下面这样</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#676E95;font-style:italic;">//场次信息</span></span>
<span class="line"><span style="color:#A6ACCD;">redisTemplate</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">opsForValue</span><span style="color:#89DDFF;">().</span><span style="color:#82AAFF;">set</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">province:cityId:secId</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">data</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">//商品信息</span></span>
<span class="line"><span style="color:#A6ACCD;">redisTemplate</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">opsForValue</span><span style="color:#89DDFF;">().</span><span style="color:#82AAFF;">set</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">secId:productId</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">data</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">//库存信息</span></span>
<span class="line"><span style="color:#A6ACCD;">redisTemplate</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">opsForValue</span><span style="color:#89DDFF;">().</span><span style="color:#82AAFF;">set</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">stock:secId:productId:password</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">data</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span></code></pre></div><p>那么你可能会说，这得存多少 Redis 的 Key 啊......的确，如果场次多一点，选择的区域多一点，是要存不少 key 。计算一下，据 2016 年统计，中国总共好像是 293 个市，按 300 算。假设最近三天有 30 场秒杀活动， 每场活动有 10 个商品 。那么总共需要的 key 数量的计算方法为 城市场次 + 场次商品 + 场次商品库存 = 300 * 30 + 30 * 10 + 30 * 10 = 9600</p><p>再按照三天内扫描前后三天再乘个三好了，也就 30000 不到的 key。你觉得这个数量多吗？我们来看看官方对于 Redis 存储 key 数量给的描述：</p><blockquote><p>Redis can handle up to 2^32 keys, and was tested in practice to handle at least 250 million &gt;keys per instance. Every hash, list, set, and sorted set, can hold 2^32 elements. In other words your limit is likely the available memory in your system.</p><p>来源于 Redis 官网</p></blockquote><p>官方说 Redis 理论上能存储 2^32 个 key ，实际测试中一个实例至少存储 2.5 亿的 key 。最后一句：你的限制其实是你系统的可用内存而已......而且这还只是一个 Redis 实例的数据。所以说不要太小看 Redis ，人家官网声称性能极高，读的速度是110000次/s，写的速度是81000次/s 。而且，如果一个互联网公司在当今缓存界对于 Redis 这么牛逼的缓存中间件的使用量很少，那么一般来说，业务用户量是有限的。不过有一点需要注意，一旦业务大量使用 Redis 作为缓存中间件，必须至少要防止三件事 <a href="http://mp.weixin.qq.com/s?__biz=MzA5ODExOTI5OA==&amp;mid=2247483720&amp;idx=1&amp;sn=b7aa67e65e2d926b19ef43c5400285ee&amp;chksm=90973aa5a7e0b3b3946fff99f1e3e1021e0aa1410af11bab823a3712aabf292e703af6c4e1fc&amp;scene=21#wechat_redirect" target="_blank" rel="noreferrer">Redis 实战应用篇 — 缓存雪崩、缓存击穿、缓存穿透和数据一致性</a></p><p>因为秒杀活动有一种业务场景是没卖完，虽然这有些尴尬......但是不得不考虑，这里需要在场次结束之后，把没有卖完的库存从 Redis 回退到库存表里面。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.23/202206281806784.png" alt="image-20220628180603721" style="zoom:67%;"><p>如上图，配置定时任务定期扫描近三天要秒杀的场次，然后上架，注意不要重复上架。上架主要是将上图的信息保存到 Redis，然后对于每个场次结束的商品发送延迟消息，在消费者里面判断如果信号量不为 0，就说明秒杀活动没有卖完，需要把库存回退，然后删除 Redis 中的信号量。</p><h2 id="秒杀商品查询" tabindex="-1">秒杀商品查询 <a class="header-anchor" href="#秒杀商品查询" aria-label="Permalink to &quot;秒杀商品查询&quot;">​</a></h2><p>由于秒杀活动查询频繁、巨大流量，千万不能去数据库查询商品信息。所有查询操作走 Redis ，注意在活动开始之前不要返回商品密码字段。</p><p>这里有一点需要注意的地方，因为页面上活动开始之前购买按钮是置灰的，所以在秒杀开始的前一秒，需要去请求一次服务器获取商品密码。假设有十万人准备抢购，那就有十万次请求发到服务器。其实十万次请求到是没什么问题，因为你既然有十万人准备抢购，就得有十万请求要到服务器，如果你在这里觉得十万次请求到服务器不太好，那么你的秒杀接口不是一样要放十万请求到服务器吗？所以关键的问题不是请求数量，而是请求的错峰。就是说你前端不能让十万客户端在真正相同毫秒级别的时间把请求发过来，比如 2021-05-01 00:00:00 有一场秒杀活动，那么前端在 2021-04-30 23:59:58 或者 59 的时候就可以发请求了，但是这里要精确到毫秒去发，1 s = 1000 ms ，前端可以在这 1000-2000 毫秒内错开十万的请求量，这样十万的请求量不在同一个毫秒级别的时间，服务器压力会小一些，而且服务器是走 Redis 查询的，响应时间应该 10 - 20 ms 就可以。拿到商品密码之后判断当前时间是否到达秒杀开始时间，如果到了就恢复按钮状态，如果没到就等时间到了再恢复按钮就行了。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.23/202206281806970.png" alt="image-20220628180635926" style="zoom:67%;"><h2 id="秒杀流程" tabindex="-1">秒杀流程 <a class="header-anchor" href="#秒杀流程" aria-label="Permalink to &quot;秒杀流程&quot;">​</a></h2><p>下面就是具体的秒杀流程详细图，按顺序描述每一节点要考虑的问题以及解决方案</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.23/202206281809289.png" alt="image-20220628180949215" style="zoom:67%;"><p>秒杀流程的伪代码：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">Autowired</span></span>
<span class="line"><span style="color:#C792EA;">private</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">RedisTemplate</span><span style="color:#89DDFF;">&lt;</span><span style="color:#C792EA;">String</span><span style="color:#89DDFF;">,</span><span style="color:#C792EA;">Object</span><span style="color:#89DDFF;">&gt;</span><span style="color:#A6ACCD;"> redisTemplate</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">/**</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;"> * 秒杀流程</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;"> * */</span></span>
<span class="line"><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">PostMapping</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">/sec-kill</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">)</span></span>
<span class="line"><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">void</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">secKill</span><span style="color:#89DDFF;">(@</span><span style="color:#C792EA;">RequestParam</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">secId</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">Long</span><span style="color:#A6ACCD;"> secId</span><span style="color:#89DDFF;">,</span></span>
<span class="line"><span style="color:#A6ACCD;">                    </span><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">RequestParam</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">productId</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">Long</span><span style="color:#A6ACCD;"> productId</span><span style="color:#89DDFF;">,</span></span>
<span class="line"><span style="color:#A6ACCD;">                    </span><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">RequestParam</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">password</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">String</span><span style="color:#A6ACCD;"> password</span><span style="color:#89DDFF;">){</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">SecResponse</span><span style="color:#A6ACCD;"> sec </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">SecResponse</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> redisTemplate</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">opsForValue</span><span style="color:#89DDFF;">().</span><span style="color:#82AAFF;">get</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">secId</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span><span style="color:#676E95;font-style:italic;">//场次信息</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">LocalDateTime</span><span style="color:#A6ACCD;"> now </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> LocalDateTime</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">now</span><span style="color:#89DDFF;">();</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">now</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">isAfter</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">sec</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">getStartTime</span><span style="color:#89DDFF;">())</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&amp;&amp;</span><span style="color:#A6ACCD;"> now</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">isBefore</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">sec</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">getEndTime</span><span style="color:#89DDFF;">())){</span><span style="color:#A6ACCD;"> </span><span style="color:#676E95;font-style:italic;">//校验已开始</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#C792EA;">SecProductResponse</span><span style="color:#A6ACCD;"> secProduct </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">SecProductResponse</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> redisTemplate</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">opsForValue</span><span style="color:#89DDFF;">()</span></span>
<span class="line"><span style="color:#A6ACCD;">                                                   </span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">get</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">secId:productId</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span><span style="color:#676E95;font-style:italic;">//场次商品信息</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">secProduct</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">getPassword</span><span style="color:#89DDFF;">().</span><span style="color:#82AAFF;">equals</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">password</span><span style="color:#89DDFF;">)){</span><span style="color:#A6ACCD;"> </span><span style="color:#676E95;font-style:italic;">//校验秒杀商品密码</span></span>
<span class="line"><span style="color:#A6ACCD;">            </span><span style="color:#C792EA;">Duration</span><span style="color:#A6ACCD;"> duration </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> Duration</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">between</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">sec</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">getStartTime</span><span style="color:#89DDFF;">(),</span><span style="color:#A6ACCD;"> sec</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">getEndTime</span><span style="color:#89DDFF;">());</span></span>
<span class="line"><span style="color:#A6ACCD;">            </span><span style="color:#C792EA;">int</span><span style="color:#A6ACCD;"> random </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#C792EA;">int</span><span style="color:#89DDFF;">)(</span><span style="color:#A6ACCD;">Math</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">random</span><span style="color:#89DDFF;">()</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">*</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">100</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">            </span><span style="color:#C792EA;">long</span><span style="color:#A6ACCD;"> period </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> duration</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">getSeconds</span><span style="color:#89DDFF;">()</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">+</span><span style="color:#A6ACCD;"> random</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#A6ACCD;">            </span><span style="color:#C792EA;">Boolean</span><span style="color:#A6ACCD;"> flag </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> stringRedisTemplate</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">opsForValue</span><span style="color:#89DDFF;">()</span></span>
<span class="line"><span style="color:#A6ACCD;">                           </span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">setIfAbsent</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">secId:productId:userId</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">1</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> period</span></span>
<span class="line"><span style="color:#A6ACCD;">                                                                     </span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> TimeUnit</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">SECONDS</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">            </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">flag </span><span style="color:#89DDFF;">!=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">null</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&amp;&amp;</span><span style="color:#A6ACCD;"> flag</span><span style="color:#89DDFF;">){</span><span style="color:#A6ACCD;"> </span><span style="color:#676E95;font-style:italic;">//校验已购买</span></span>
<span class="line"><span style="color:#A6ACCD;">                </span><span style="color:#C792EA;">RSemaphore</span><span style="color:#A6ACCD;"> semaphore </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> redissonClient</span></span>
<span class="line"><span style="color:#A6ACCD;">                                       </span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">getSemaphore</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">secId:productId:password</span><span style="color:#89DDFF;">&quot;</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">                </span><span style="color:#89DDFF;font-style:italic;">try</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#89DDFF;">                    </span><span style="color:#676E95;font-style:italic;">//尝试在50ms内获取信号量</span></span>
<span class="line"><span style="color:#A6ACCD;">                    </span><span style="color:#C792EA;">boolean</span><span style="color:#A6ACCD;"> acquire </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> semaphore</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">tryAcquire</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">num</span><span style="color:#89DDFF;">,</span><span style="color:#F78C6C;">50</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> TimeUnit</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">MILLISECONDS</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">                    </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">acquire</span><span style="color:#89DDFF;">){</span><span style="color:#A6ACCD;"> </span><span style="color:#676E95;font-style:italic;">//抢到库存</span></span>
<span class="line"><span style="color:#A6ACCD;">                        </span><span style="color:#C792EA;">String</span><span style="color:#A6ACCD;"> orderNo </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">generateOrderNo</span><span style="color:#89DDFF;">();</span><span style="color:#676E95;font-style:italic;">//生成订单号</span></span>
<span class="line"><span style="color:#A6ACCD;">                        rabbitTemplate</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">convertAndSend</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">hosjoy-b2b-secKill</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span></span>
<span class="line"><span style="color:#A6ACCD;">                                                      </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">routingKey</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span></span>
<span class="line"><span style="color:#A6ACCD;">                                                      </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">data</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span><span style="color:#676E95;font-style:italic;">//发送消息</span></span>
<span class="line"><span style="color:#A6ACCD;">                    </span><span style="color:#89DDFF;">}</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">else</span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#89DDFF;">                        </span><span style="color:#676E95;font-style:italic;">//如果没抢到，删除已购买的标识(其实不删也没什么问题)</span></span>
<span class="line"><span style="color:#A6ACCD;">                        stringRedisTemplate</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">delete</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">secId:productId:userId</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">                    </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">                </span><span style="color:#89DDFF;">}</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">catch</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#C792EA;">InterruptedException</span><span style="color:#A6ACCD;"> </span><span style="color:#A6ACCD;font-style:italic;">e</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{}</span></span>
<span class="line"><span style="color:#A6ACCD;">            </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;">}</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">else</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">            stringRedisTemplate</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">opsForValue</span><span style="color:#89DDFF;">().</span><span style="color:#82AAFF;">increment</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">black:secId:productId:userId</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><p>以上就是大致的秒杀流程代码，也是我觉得比较好的秒杀流程，设计完之后请同事大佬指点（咳咳，其实我是想装个 X，嘘！）了一下。我与他的想法或者说设计思路有主要两点不同</p><ul><li>实现 Redis 库存的数据结构</li><li>什么时候算秒杀成功</li></ul><p>他使用的是 Redis 的 List 数据结构来存放库存，比如有 100 个库存就 leftPush 100 个商品 id。然后通过 pop 的方式去扣减库存。我对比了一下分布式信号量 Semaphore 和 List 结构，两者都可以实现，用起来也都很方便，还有个 incr 和 decr 自增自减其实也可以，但是这都是默认针对秒杀商品只能秒杀一件的 。如果说业务允许秒杀可以购买多件商品，那么 List 和 decr 就必须要加分布式锁来控制了，如此一来会让系统的吞吐量就相对被降低了。因为 List 一次只能弹出一个元素，decr 虽然可以传参数扣减，但是可以减到负数的。假设 A 用户秒杀 5 件，库存现在只有 4 件，B 用户秒杀 2 件，理论上 A 是秒杀失败，但是 B 应该秒杀成功，如果不加分布式锁，A 把库存减到 -1 ，发现不对，要把库存加回去，此时 B 秒杀 2 件，发现库存已经是 -1 了，也秒杀失败，这就有问题了。所以......分布式信号量牛逼啊！</p><p>还有个区别是他有个用户购买之后排队的概念来校验重复购买，我这直接 setIfAbsent 来校验，这个区别其实无关紧要，重要的是什么时候算秒杀成功。</p><h2 id="我的设计思路" tabindex="-1">我的设计思路 <a class="header-anchor" href="#我的设计思路" aria-label="Permalink to &quot;我的设计思路&quot;">​</a></h2><blockquote><p>以我的设计方案，只要用户尝试获取信号量成功，就算秒杀成功，但是这里其实可以不用立即返回告诉用户，最好让用户手机继续转圈 1-2 秒之后告诉他秒杀成功，因为 MQ 发送消息到消费成功有一定时间，如果立即告诉用户秒杀成功，而订单还在生成中，可能会给用户带来不好的用户体验。等 1-2 秒之后 MQ 消息消费完成订单也生成成功，此时正好用户收到秒杀成功，订单也生成成功就很 NICE！</p></blockquote><blockquote><p>那么你可能会有疑问，如果消费者生成订单报错了怎么办？不得不说，这是个必须考虑的问题，毕竟 MQ 的消费说不准。这里当然我也考虑到了这种情况，如果消费失败首先采取重试，如果重试 3 次仍然失败，那说明这里产生了代码问题导致订单生成失败，记录下来报错消息，然后人工查询错误，恢复用户订单即可。毕竟这是个小概率的事情，也不会有一堆订单消费失败吧？更何况人家本来就是在秒杀服务抢到了库存，既然抢到了我就算他秒杀成功了，订单由于其他原因生成失败，我给他手动生成订单，保证最终一致性即可，不然怎么跟用户交代？</p></blockquote><h2 id="同事的设计思路" tabindex="-1">同事的设计思路 <a class="header-anchor" href="#同事的设计思路" aria-label="Permalink to &quot;同事的设计思路&quot;">​</a></h2><p>而同事他说不应该这么设计，应该设计为用户抢到信号量只是有一个秒杀机会，具体秒杀成功与否要看订单服务消费的结果。如果订单服务消费失败，就回滚秒杀库存到 Redis ，让其他用户来抢，因为可能会存在业务校验不通过，用户没有购买资格。不得不说，他考虑问题一向很周到，我从跟他后面做项目开始到现在也成长很多，他真的是实力很强的大佬！</p><p>不过我的设计初衷是没有考虑到有业务校验用户没有资格购买商品，为什么会有用户没有资格购买商品……这特么什么业务场景，既然没资格买为什么要让他看到秒杀活动？。但是仔细一想，这样根据订单生成结果判定秒杀结果其实是有点问题的。</p><h3 id="存在的问题" tabindex="-1">存在的问题 <a class="header-anchor" href="#存在的问题" aria-label="Permalink to &quot;存在的问题&quot;">​</a></h3><ul><li>假设 100 万人抢 400 茅台，本来全部抢完之后你提醒没抢到的用户秒杀商品已抢完了。但是订单服务那边消费到第 399 和 400 个消息的时候失败了，回滚了订单，回滚了库存到 Redis 。如果是因为业务校验未通过，那我认为是否应该不让用户看见这个活动，或者想办法在抢到秒杀机会之前就提示用户没有参加资格会比较好</li><li>此时消费到第 399 和 400 消息大约过了 3-5 秒，你把它回滚了。正常用户刚开始看没抢到，可能都走了，这还有可能发生少卖。</li><li>如果不是因为业务校验的问题，而是代码问题导致的报错，这时回滚了订单，感觉这个用户有点惨啊，明明是系统问题，却让用户背锅......</li><li>如果该用户由于代码问题被回滚了订单，然后去秒杀商品页面又看到了库存剩余再次秒杀，然后再次失败，再次秒杀，再次失败......如此循环下去，我觉得他的内心是崩溃的......，不过这个概率很小</li></ul><p>看到这里大伙可能会觉得，我靠这个博主太不要脸了，就挑别人的刺，不考虑自己的问题</p><p>emmm 我怎么会是这种人呢......</p><h3 id="我的方案存在的问题" tabindex="-1">我的方案存在的问题 <a class="header-anchor" href="#我的方案存在的问题" aria-label="Permalink to &quot;我的方案存在的问题&quot;">​</a></h3><blockquote><ul><li>需要有人去关注秒杀活动，虽然出错的概率比较小，但是一旦订单服务报错，你得有人去尽快生成/恢复订单，耗费人力。如果恢复了订单，用户最后不支付的话，那这个人力资源相当于白费了呀。。。</li><li>未支付就在设计逻辑上算用户秒杀成功，这样可能领导听起来不太能接受，如果先让用户支付，支付完成才算秒杀成功，然后去生成订单，这样领导应该会很赞同......这个看起来没问题，实际实现细节上有没有问题还没有研究过，毕竟天猫、淘宝也是先生成订单才去支付的，等第二版更新。</li></ul></blockquote><p>个人觉得每个人的方案都可能存在一定的局限、问题，毕竟没有完美的方案，只能最后根据实际业务情况或者公司所有同事一起讨论去选用一种更为符合的设计方案，或者在此基础上再做优化。</p><h1 id="秒杀系统设计要点" tabindex="-1">秒杀系统设计要点 <a class="header-anchor" href="#秒杀系统设计要点" aria-label="Permalink to &quot;秒杀系统设计要点&quot;">​</a></h1><p>秒杀，就像是计划经济的菜市场，过客匆匆，你来我往。熙熙攘攘一阵子，过后只留下冷清寂寞的大街。</p><p>且看一个卖鹅的故事。↓↓↓</p><p>就在昨天。天刚蒙蒙亮，大概是”早晨五六点钟“，几个程序员顶着蓬松的头发，下班结伴而行。这个时候，街上的路灯还没有灭的干脆。几个锻炼的老大爷，叉着腰，身体前倾，弯成一张弓。</p><p>所以，胡同岔口里一个卖鹅的小贩，就显得特别的显眼。就见一大群鹅被关在诺大的笼子里，扑棱着翅膀。有几只精力旺盛的，伸长脖子呱呱叫着，小贩听得心烦，就用手中的树枝敲它们的脑袋。</p><p>小贩歪了歪嘴，又用眼角余光扫了下手机，已经五点六十了。就在这时，四面八方就围上来一群大妈，就像是从地底冒出来一样。刚开始还悠然的靠拢，等看到笼子果然有一群鹅，就捏着手里的小包，争先恐后的小跑起来。</p><p>**这来势汹汹的阵势，吓了大家一跳，程序员们站住不动了。**大爷的腰也不弯了，就连那些聒噪的鹅，也不叫了。</p><p>很快就有大妈帮小贩打开了笼子，不容分说，扯住一只鹅的脖子就拖了出来。接着就有另外一只手扯住了另一只鹅的翅膀。一下子人喊鹅嘶，吵吵嚷嚷，下起了鹅毛大雪。不一会儿，所有的鹅就都在大妈们手里了。但也有更多没得到鹅的大妈，用手绢在一旁抹着悔恨嫉妒的泪水。</p><p>王大妈最高兴了，她手大，抓了三只，其中两只倒霉的鹅被她握在一只手里，脖子拧成一根麻花。也有被捏死的鹅，犯了鹅命的大妈就不想要了，但有更多的大妈根本就不嫌弃。</p><p><strong>小贩长吁一口气，招数确实有效，就搞了个秒杀，这群犯了瘟病的鹅，瞬间被低价处理了。</strong></p><h2 id="秒杀难点" tabindex="-1">秒杀难点 <a class="header-anchor" href="#秒杀难点" aria-label="Permalink to &quot;秒杀难点&quot;">​</a></h2><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207142119905.png" alt="image-20220714211904840" style="zoom:67%;"><p><strong>1、流量超出承载范围</strong> 秒杀，一般归作<code>突发流量</code>，平常一天的请求量，可能集中在几秒之内就能完成。秒杀资源的稀缺，也会造成资源成为热点，发生多人争抢的局面。想象一下小长假的高速公路收费站，就能够了解到一无所得的参与者有多火大。</p><p><strong>2、资源冲突</strong> 如果采用传统的数据库进行数据存储，对同一资源的争抢，就会面临严重的锁冲突问题。一般是通过一个前置的，速度更快的存储顶在前面，这就涉及到源库和目标库的数据同步问题。</p><p>从商品资源的上架，到秒杀的完成，会经历一个短暂的混沌状态，出现数据不一致的情况。在请求量非常集中的情况下，还会产生并发问题，个体的行为和结果，是不可预测的。</p><p><strong>3、难度高</strong> 秒杀对基础设施和技术的要求也是比较高的，从接入层到缓存到存储层，以及安全方面的考虑，需要多个组件的参与，而且每个组件都需要进行优化。</p><p>总之，吵吵闹闹一场，最终会归于平静。为了秒杀而准备的硬件资源，不能就放在那里闲置了吧，所以一般还会有一个资源释放阶段，这是后话，我们不做过多关注。</p><h2 id="业务三阶段" tabindex="-1">业务三阶段 <a class="header-anchor" href="#业务三阶段" aria-label="Permalink to &quot;业务三阶段&quot;">​</a></h2><p>一般的，秒杀业务会分为三个阶段。其中，抢购阶段，就是我们常说的秒杀业务。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207142119951.png" alt="image-20220714211949891" style="zoom:67%;"><p><strong>1、准备阶段</strong> 在准备阶段，除了硬件和软件系统的准备，一般会有一个活动上的预热，互联网运营会将类似电线杆上小广告的东西，广而发之。从前，有个客户搞了个秒杀活动，100个库存99个人参与，99个人有90个内部员工，尴尬呵呵。</p><p>如果有自己的app，通过通知、订阅，会达到比较好的效果。如果涉及的商品多，参与人数巨大，会对数据进行预处理，进行提前预热。一切准备妥当，就可以抽根烟，等着倒计时了。</p><p><strong>2、抢购</strong> 俗话说，台上一分钟，台下十年功。秒杀开始，会有大量的瞬间请求涌入，该到了上台表演的时候了。这个阶段，我们的每个系统和模块，都会迅速轮转起来，任何一个点考虑不周，都会造成本次秒杀活动的失败，所以关键组件要保证极高可用。</p><p><strong>3、结束清算</strong> 上面也说过，秒杀会有一个短暂的混沌态。清算阶段，就要完成数据的最终一致性，落库动作可能会持续不少时间。可能有的用户，在付款的那一刻，后悔了，商品要重新归位回仓。回仓后的商品一般会再次售卖，比如火车票，30分钟后可以再抢一次；有的商品就可以被锁定下架，永远消失了。</p><h2 id="制约原理" tabindex="-1">制约原理 <a class="header-anchor" href="#制约原理" aria-label="Permalink to &quot;制约原理&quot;">​</a></h2><p>这里，我们大体说一下秒杀系统在技术方面的基本制约原理，详细的描述和代码，我们在后面的章节里进行说明。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207142120112.png" alt="image-20220714212033015" style="zoom:60%;"><h3 id="数据预热" tabindex="-1">数据预热 <a class="header-anchor" href="#数据预热" aria-label="Permalink to &quot;数据预热&quot;">​</a></h3><p>有的系统进行秒杀的，可能就那么几件商品，手工录入都玩得转。对于平台类型，或者用户量巨大的app，秒杀的商品就不再那么简单。</p><p>中间，会进行一些数据的合并，或者二维展开，也就是准备秒杀的数据。这些数据进行处理之后，会提前进入到秒杀系统进行数据预热。</p><p>秒杀阶段进行的是否顺利，得看数据准备的是否合理。</p><h3 id="请求承载" tabindex="-1">请求承载 <a class="header-anchor" href="#请求承载" aria-label="Permalink to &quot;请求承载&quot;">​</a></h3><p>这是请求的最外层，属于接入层。做的好的系统，能够在接入层就屏蔽大部分请求，极大的减轻后端服务的压力。</p><p><strong>连接数承载</strong> 对于接入层来说，首先一个挑战就是连接数。一般互联网的接入层，就是lvs+nginx。这里会涉及到对操作系统的优化，以及对于nginx本身的优化。</p><p><strong>并发承载</strong> 对于落到某台机器上的请求，依然会存在并发高的问题。线程池使用要合理，怎么去进行过滤，合并等，有一定挑战。</p><p><strong>负载均衡</strong> 请求要能真正的做到均衡，不能产生热点问题。比如nginx的ip_hash，虽然能一定程度上规避分布式session问题，但请求会不均衡。</p><p><strong>重试？</strong> 秒杀业务不要配置重试，会加剧系统负载。请求失败？那就再来一次。</p><p><strong>系统隔离</strong> 秒杀业务占用的系统资源，和正常运行的系统严重不对等。如果有条件，秒杀系统的硬件和服务环境，要与正常业务系统进行一定程度的隔离。要提前评估对其他服务的压力，避免影响正常业务。</p><p><strong>CDN</strong> 对于html，js，css，图片等内容，占用了大量的带宽。如果将这些资源都放在自己的服务器上，流量到来时会迅速占满带宽，造成正常的秒杀请求无法完成。CDN可以有效解决这个问题。剩下的请求，就是真正的秒杀业务。</p><p><strong>减小请求包</strong> 对网络请求包，要进行大量优化。可以开gzip压缩，资源本身也进行压缩，去掉请求包中的无用信息，对网络报文进行精简。</p><h3 id="请求拦截" tabindex="-1">请求拦截 <a class="header-anchor" href="#请求拦截" aria-label="Permalink to &quot;请求拦截&quot;">​</a></h3><p>秒杀系统一个非常大的原则，就是要把尽量多的无效请求，拦截在外部。请求拦截，可以分为上游拦截和业务拦截。</p><p><strong>上游拦截</strong> 拦截方面，存在一个全局的设置。当系统判断以及达到瓶颈阶段，就可以通过全局限流方式进行服务降级，对于一些次级服务，要进行熔断处理。对于前端来说，也是需要进行一些优化的。比如浏览器缓存，防重入验证等，能够拦截数量可观的请求。</p><p><strong>业务拦截</strong> 除了一些全局的限制，对于大部分请求来说，和用户是息息相关的。一个用户，可能会频繁的刷新，或者绕过前端，直接使用软件调用后端接口。这些用户的非法请求，也要进行拦截。</p><p>同时，用户对资源的争夺，也不应该无限等待。比如100个库存的商品，第1w个请求到来的时候，就不需要再排队等待了，直接返回秒杀完毕就ok了。</p><p>排队方面，会用到jvm内排队，也会用到外部的消息队列，mq，进行请求的缓冲。</p><h3 id="数据缓存" tabindex="-1">数据缓存 <a class="header-anchor" href="#数据缓存" aria-label="Permalink to &quot;数据缓存&quot;">​</a></h3><p>缓存，可能是秒杀系统中最重要的一个组件了。从前端缓存，到jvm缓存，再到分布式缓存，都会对系统性能产生数量级的提升。值得注意的是，由于秒杀系统严重依赖缓存系统，所以缓存系统需要做高可用。</p><p>缓存的读操作，要考虑数据的加载和同步。写请求，就要考虑数据的合并与并发写入，数据的一致性等。虽然缓存的速度比起DB来，快了很多很多，但它的性能总是有瓶颈的，相关代码要着重优化。</p><h3 id="安全" tabindex="-1">安全 <a class="header-anchor" href="#安全" aria-label="Permalink to &quot;安全&quot;">​</a></h3><p>技术的门槛越来越低，二年级的小学生都开课教swift了，写个秒杀插件什么的，不费吹灰之力。秒杀系统的安全性比较重要，应该说和钱打交道的系统，都是容易出问题的。你要是想薅羊毛，认准营销、秒杀业务，准没错。</p><p>要尽量提高作弊门槛，比如url动态化，从入口就隔绝了大部分攻击；验证码，只会增加攻击者的成本。有些安全性级别较高的，还会增加风控规则，比如同一ip请求过多封禁、账号注册日期三天之内不允许参与、秒杀的门槛必须是金牌会员等。</p><p>我曾经经历过一次，对方本来是一个算加减乘除的验证码，脚本都写好了。结果秒杀前5分钟，验证码12306附体，xjjdog直接放弃了。</p><h3 id="躲在幕后的db" tabindex="-1">躲在幕后的DB <a class="header-anchor" href="#躲在幕后的db" aria-label="Permalink to &quot;躲在幕后的DB&quot;">​</a></h3><p>在整个秒杀系统中，传统的DB，只能灰溜溜的躲在幕后（小流量除外）。我要是DB，也会躲在后面瑟瑟发抖。</p><p>DB的数据，要提前载入到秒杀系统中进行运算，秒杀完毕，还要把狼藉的数据进行落地与清算。在笔者见过的不少秒杀场景中，甚至不需要DB的参与，真是艺高人胆大。</p><p>这就引申出另外一个问题。假如缓存系统出现问题，请求要不要穿透呢？我的建议是，不需要。非正常的请求，会瞬间压垮DB，产生更加严重的数据错乱问题，假如没有做隔离，后果会更狂野。与其错了，不如认怂，乖乖的复盘写故障报告吧。</p><h2 id="end" tabindex="-1">End <a class="header-anchor" href="#end" aria-label="Permalink to &quot;End&quot;">​</a></h2><p>秒杀，夺宝，p2p，是互联网创造的，钱袋子三大杀手。</p><p>我要说一些隐秘的事情。前不久，我的这群鹅本来好好的，结果混进一只长了瘟病的鸡，没几天就病恹恹的不行了，要是扔了怪可惜的。这卖鹅的时间线，也搞的十分紧凑。</p><p>就在昨天，我就放出了有一批廉价鹅要处理的消息。为了让更多的大妈相信，我按照市面价格打了个五折，其实打一折都能出手（抖音上那种鹅不敢卖）。**5点多我就给几只快不成的鹅，注射了兴奋剂，希望它们能多撑一会，还拿了根树枝敲它们的脑袋进行确认。****不是我自信，这种场景，就是死鹅也卖得出去。**但死的多了，毕竟不好。为了限制拥挤的人流，我特意把笼子口的铁丝角给漏出来，不少大妈划破了手都没把鹅抓到，我也搞不清她们是不幸，还是幸运。</p><p>是谁首先创造的秒杀？真是天才。和饥饿营销一样，收的是智商税吧。哈哈哈~</p><h1 id="面霸篇-秒杀系统如何设计⭐⭐" tabindex="-1">面霸篇：秒杀系统如何设计⭐⭐ <a class="header-anchor" href="#面霸篇-秒杀系统如何设计⭐⭐" aria-label="Permalink to &quot;面霸篇：秒杀系统如何设计⭐⭐&quot;">​</a></h1><p><a href="https://mp.weixin.qq.com/s?__biz=MzkzMDI1NjcyOQ==&amp;mid=2247491158&amp;idx=1&amp;sn=5ad3081c25dace2743b63de87d2f798f&amp;chksm=c27c5c60f50bd57632d30ae083322e47b3b0ab2af973e5111b3d0b0346d45b7fe67af0a2ee83&amp;mpshare=1&amp;scene=23&amp;srcid=0813JXqSdpGqnGLUywN0CXLj&amp;sharer_sharetime=1660375024536&amp;sharer_shareid=29b8a04db1dbd975e3bf4e9f47e7ac67#rd" target="_blank" rel="noreferrer">面霸篇：秒杀系统如何设计 (qq.com)</a></p><h2 id="前言-1" tabindex="-1">前言 <a class="header-anchor" href="#前言-1" aria-label="Permalink to &quot;前言&quot;">​</a></h2><p>高并发下如何设计秒杀系统？这是一个高频面试题。这个问题看似简单，但是里面的水很深，它考查的是高并发场景下，从前端到后端多方面的知识。</p><p>秒杀一般出现在商城的<code>促销活动</code>中，指定了一定数量（比如：10个）的商品（比如：手机），以极低的价格（比如：0.1元），让大量用户参与活动，但只有极少数用户能够购买成功。这类活动商家绝大部分是不赚钱的，说白了是找个噱头宣传自己。</p><p>虽说秒杀只是一个促销活动，但对技术要求不低。下面给大家总结一下设计秒杀系统需要注意的9个细节。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131739368.png" alt="image-20220813173915263" style="zoom:50%;"><h2 id="_1-瞬时高并发" tabindex="-1">1 瞬时高并发 <a class="header-anchor" href="#_1-瞬时高并发" aria-label="Permalink to &quot;1 瞬时高并发&quot;">​</a></h2><p>一般在<code>秒杀时间点</code>（比如：12点）前几分钟，用户并发量才真正突增，达到秒杀时间点时，并发量会达到顶峰。</p><p>但由于这类活动是大量用户抢少量商品的场景，必定会出现<code>狼多肉少</code>的情况，所以其实绝大部分用户秒杀会失败，只有极少部分用户能够成功。</p><p>正常情况下，大部分用户会收到商品已经抢完的提醒，收到该提醒后，他们大概率不会在那个活动页面停留了，如此一来，用户并发量又会急剧下降。所以这个峰值持续的时间其实是非常短的，这样就会出现瞬时高并发的情况，下面用一张图直观的感受一下流量的变化：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131739115.png" alt="image-20220813173934050" style="zoom:67%;"><p>像这种瞬时高并发的场景，传统的系统很难应对，我们需要设计一套全新的系统。可以从以下几个方面入手：</p><ol><li>页面静态化</li><li>CDN加速</li><li>缓存</li><li>mq异步处理</li><li>限流</li><li>分布式锁</li></ol><h2 id="_2-页面静态化" tabindex="-1">2. 页面静态化 <a class="header-anchor" href="#_2-页面静态化" aria-label="Permalink to &quot;2. 页面静态化&quot;">​</a></h2><p>活动页面是用户流量的第一入口，所以是并发量最大的地方。</p><p>如果这些流量都能直接访问服务端，恐怕服务端会因为承受不住这么大的压力，而直接挂掉。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131739846.png" alt="image-20220813173947737" style="zoom:50%;"><p>活动页面绝大多数内容是固定的，比如：商品名称、商品描述、图片等。为了减少不必要的服务端请求，通常情况下，会对活动页面做<code>静态化</code>处理。用户浏览商品等常规操作，并不会请求到服务端。只有到了秒杀时间点，并且用户主动点了秒杀按钮才允许访问服务端。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131740798.png" alt="image-20220813174002697" style="zoom:67%;"><p>这样能过滤大部分无效请求。</p><p>但只做页面静态化还不够，因为用户分布在全国各地，有些人在北京，有些人在成都，有些人在深圳，地域相差很远，网速各不相同。</p><p>如何才能让用户最快访问到活动页面呢？</p><p>这就需要使用CDN，它的全称是Content Delivery Network，即内容分发网络。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131740588.png" alt="image-20220813174016451" style="zoom:67%;"><p>使用户就近获取所需内容，降低网络拥塞，提高用户访问响应速度和命中率。</p><h2 id="_3-秒杀按钮" tabindex="-1">3 秒杀按钮 <a class="header-anchor" href="#_3-秒杀按钮" aria-label="Permalink to &quot;3 秒杀按钮&quot;">​</a></h2><p>大部分用户怕错过<code>秒杀时间点</code>，一般会提前进入活动页面。此时看到的<code>秒杀按钮</code>是置灰，不可点击的。只有到了秒杀时间点那一时刻，秒杀按钮才会自动点亮，变成可点击的。</p><p>但此时很多用户已经迫不及待了，通过不停刷新页面，争取在第一时间看到秒杀按钮的点亮。</p><p>从前面得知，该活动页面是静态的。那么我们在静态页面中如何控制秒杀按钮，只在秒杀时间点时才点亮呢？</p><p>没错，使用js文件控制。</p><p>为了性能考虑，一般会将css、js和图片等静态资源文件提前缓存到CDN上，让用户能够就近访问秒杀页面。</p><p>看到这里，有些聪明的小伙伴，可能会问：CDN上的js文件是如何更新的？</p><p>秒杀开始之前，js标志为false，还有另外一个随机参数。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131741882.png" alt="image-20220813174159754" style="zoom:67%;"><p>当秒杀开始的时候系统会生成一个新的js文件，此时标志为true，并且随机参数生成一个新值，然后同步给CDN。由于有了这个随机参数，CDN不会缓存数据，每次都能从CDN中获取最新的js代码。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131742395.png" alt="image-20220813174214266" style="zoom:67%;"><p>此外，前端还可以加一个定时器，控制比如：10秒之内，只允许发起一次请求。如果用户点击了一次秒杀按钮，则在10秒之内置灰，不允许再次点击，等到过了时间限制，又允许重新点击该按钮。</p><h2 id="_4-读多写少" tabindex="-1">4 读多写少 <a class="header-anchor" href="#_4-读多写少" aria-label="Permalink to &quot;4 读多写少&quot;">​</a></h2><p>在秒杀的过程中，系统一般会先查一下库存是否足够，如果足够才允许下单，写数据库。如果不够，则直接返回该商品已经抢完。</p><p>由于大量用户抢少量商品，只有极少部分用户能够抢成功，所以绝大部分用户在秒杀时，库存其实是不足的，系统会直接返回该商品已经抢完。</p><p>这是非常典型的：<code>读多写少</code> 的场景。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131742435.png" alt="image-20220813174232339" style="zoom:50%;"><p>如果有数十万的请求过来，同时通过数据库查缓存是否足够，此时数据库可能会挂掉。因为数据库的连接资源非常有限，比如：mysql，无法同时支持这么多的连接。</p><p>而应该改用缓存，比如：redis。</p><p>即便用了redis，也需要部署多个节点。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131742610.png" alt="image-20220813174250496" style="zoom:50%;"><h2 id="_5-缓存问题" tabindex="-1">5 缓存问题 <a class="header-anchor" href="#_5-缓存问题" aria-label="Permalink to &quot;5 缓存问题&quot;">​</a></h2><p>通常情况下，我们需要在redis中保存商品信息，里面包含：商品id、商品名称、规格属性、库存等信息，同时数据库中也要有相关信息，毕竟缓存并不完全可靠。</p><p>用户在点击秒杀按钮，请求秒杀接口的过程中，需要传入的商品id参数，然后服务端需要校验该商品是否合法。</p><p>大致流程如下图所示：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131743267.png" alt="image-20220813174305181" style="zoom:50%;"><p>根据商品id，先从缓存中查询商品，如果商品存在，则参与秒杀。如果不存在，则需要从数据库中查询商品，如果存在，则将商品信息放入缓存，然后参与秒杀。如果商品不存在，则直接提示失败。</p><p>这个过程表面上看起来是OK的，但是如果深入分析一下会发现一些问题。</p><h3 id="_5-1-缓存击穿" tabindex="-1">5.1 缓存击穿 <a class="header-anchor" href="#_5-1-缓存击穿" aria-label="Permalink to &quot;5.1 缓存击穿&quot;">​</a></h3><p>比如商品A第一次秒杀时，缓存中是没有数据的，但数据库中有。虽说上面有如果从数据库中查到数据，则放入缓存的逻辑。</p><p>然而，在高并发下，同一时刻会有大量的请求，都在秒杀同一件商品，这些请求同时去查缓存中没有数据，然后又同时访问数据库。结果悲剧了，数据库可能扛不住压力，直接挂掉。</p><p>如何解决这个问题呢？</p><p>这就需要加锁，最好使用分布式锁。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131743961.png" alt="image-20220813174320875" style="zoom:67%;"><p>当然，针对这种情况，最好在项目启动之前，先把缓存进行<code>预热</code>。即事先把所有的商品，同步到缓存中，这样商品基本都能直接从缓存中获取到，就不会出现缓存击穿的问题了。</p><p>是不是上面加锁这一步可以不需要了？</p><p>表面上看起来，确实可以不需要。但如果缓存中设置的过期时间不对，缓存提前过期了，或者缓存被不小心删除了，如果不加速同样可能出现缓存击穿。</p><p>其实这里加锁，相当于买了一份保险。</p><h3 id="_5-2-缓存穿透" tabindex="-1">5.2 缓存穿透 <a class="header-anchor" href="#_5-2-缓存穿透" aria-label="Permalink to &quot;5.2 缓存穿透&quot;">​</a></h3><p>如果有大量的请求传入的商品id，在缓存中和数据库中都不存在，这些请求不就每次都会穿透过缓存，而直接访问数据库了。</p><p>由于前面已经加了锁，所以即使这里的并发量很大，也不会导致数据库直接挂掉。</p><p>但很显然这些请求的处理性能并不好，有没有更好的解决方案？</p><p>这时可以想到<code>布隆过滤器</code>。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131743888.png" alt="image-20220813174357802" style="zoom:50%;"><p>系统根据商品id，先从布隆过滤器中查询该id是否存在，如果存在则允许从缓存中查询数据，如果不存在，则直接返回失败。</p><p>虽说该方案可以解决缓存穿透问题，但是又会引出另外一个问题：布隆过滤器中的数据如何更缓存中的数据保持一致？</p><p>这就要求，如果缓存中数据有更新，则要及时同步到布隆过滤器中。如果数据同步失败了，还需要增加重试机制，而且跨数据源，能保证数据的实时一致性吗？</p><p>显然是不行的。</p><p>所以布隆过滤器绝大部分使用在缓存数据更新很少的场景中。</p><p>如果缓存数据更新非常频繁，又该如何处理呢？</p><p>这时，就需要把不存在的商品id也缓存起来。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131744219.png" alt="image-20220813174415131" style="zoom:50%;"><p>下次，再有该商品id的请求过来，则也能从缓存中查到数据，只不过该数据比较特殊，表示商品不存在。需要特别注意的是，这种特殊缓存设置的超时时间应该尽量短一点。</p><h2 id="_6-库存问题" tabindex="-1">6 库存问题 <a class="header-anchor" href="#_6-库存问题" aria-label="Permalink to &quot;6 库存问题&quot;">​</a></h2><p>对于库存问题看似简单，实则里面还是有些东西。</p><p>真正的秒杀商品的场景，不是说扣完库存，就完事了，如果用户在一段时间内，还没完成支付，扣减的库存是要加回去的。</p><p>所以，在这里引出了一个<code>预扣库存</code>的概念，预扣库存的主要流程如下：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131745360.png" alt="image-20220813174519260" style="zoom:50%;"><p>扣减库存中除了上面说到的<code>预扣库存</code>和<code>回退库存</code>之外，还需要特别注意的是库存不足和库存超卖问题。</p><h3 id="_6-1-数据库扣减库存" tabindex="-1">6.1 数据库扣减库存 <a class="header-anchor" href="#_6-1-数据库扣减库存" aria-label="Permalink to &quot;6.1 数据库扣减库存&quot;">​</a></h3><p>使用数据库扣减库存，是最简单的实现方案了，假设扣减库存的sql如下：</p><div class="language-sql"><button title="Copy Code" class="copy"></button><span class="lang">sql</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#F78C6C;">update</span><span style="color:#A6ACCD;"> product </span><span style="color:#F78C6C;">set</span><span style="color:#A6ACCD;"> stock</span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;">stock</span><span style="color:#89DDFF;">-</span><span style="color:#F78C6C;">1</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">where</span><span style="color:#A6ACCD;"> id</span><span style="color:#89DDFF;">=</span><span style="color:#F78C6C;">123</span><span style="color:#A6ACCD;">;</span></span></code></pre></div><p>这种写法对于扣减库存是没有问题的，但如何控制库存不足的情况下，不让用户操作呢？</p><p>这就需要在update之前，先查一下库存是否足够了。</p><p>伪代码如下：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#C792EA;">int</span><span style="color:#A6ACCD;"> stock </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> mapper</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">getStockById</span><span style="color:#89DDFF;">(</span><span style="color:#F78C6C;">123</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">stock </span><span style="color:#89DDFF;">&gt;</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">0</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#C792EA;">int</span><span style="color:#A6ACCD;"> count </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> mapper</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">updateStock</span><span style="color:#89DDFF;">(</span><span style="color:#F78C6C;">123</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">count </span><span style="color:#89DDFF;">&gt;</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">0</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#82AAFF;">addOrder</span><span style="color:#89DDFF;">(</span><span style="color:#F78C6C;">123</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><p>大家有没有发现这段代码的问题？</p><p>没错，查询操作和更新操作不是原子性的，会导致在并发的场景下，出现库存超卖的情况。</p><p>有人可能会说，这样好办，加把锁，不就搞定了，比如使用synchronized关键字。</p><p>确实，可以，但是性能不够好。</p><p>还有更优雅的处理方案，即基于数据库的乐观锁，这样会少一次数据库查询，而且能够天然的保证数据操作的原子性。</p><p>只需将上面的sql稍微调整一下：</p><div class="language-sql"><button title="Copy Code" class="copy"></button><span class="lang">sql</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#F78C6C;">update</span><span style="color:#A6ACCD;"> product </span><span style="color:#F78C6C;">set</span><span style="color:#A6ACCD;"> stock</span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;">stock</span><span style="color:#89DDFF;">-</span><span style="color:#F78C6C;">1</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">where</span><span style="color:#A6ACCD;"> id</span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;">product </span><span style="color:#F78C6C;">and</span><span style="color:#A6ACCD;"> stock </span><span style="color:#89DDFF;">&gt;</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">0</span><span style="color:#A6ACCD;">;</span></span></code></pre></div><p>在sql最后加上：<code>stock &gt; 0</code>，就能保证不会出现超卖的情况。</p><p>但需要频繁访问数据库，我们都知道数据库连接是非常昂贵的资源。在高并发的场景下，可能会造成系统雪崩。而且，容易出现多个请求，同时竞争行锁的情况，造成相互等待，从而出现死锁的问题。</p><h3 id="_6-2-redis扣减库存" tabindex="-1">6.2 redis扣减库存 <a class="header-anchor" href="#_6-2-redis扣减库存" aria-label="Permalink to &quot;6.2 redis扣减库存&quot;">​</a></h3><p>redis的<code>incr</code>方法是原子性的，可以用该方法扣减库存。伪代码如下：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#C792EA;">boolean</span><span style="color:#A6ACCD;"> exist </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> redisClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">query</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;">userId</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">exist</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">-</span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#C792EA;">int</span><span style="color:#A6ACCD;"> stock </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> redisClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">queryStock</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productId</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">stock </span><span style="color:#89DDFF;">&lt;=</span><span style="color:#F78C6C;">0</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">0</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">  redisClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">incrby</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">-</span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  redisClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">add</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;">userId</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">;</span></span></code></pre></div><p>代码流程如下：</p><ol><li>先判断该用户有没有秒杀过该商品，如果已经秒杀过，则直接返回-1。</li><li>查询库存，如果库存小于等于0，则直接返回0，表示库存不足。</li><li>如果库存充足，则扣减库存，然后将本次秒杀记录保存起来。然后返回1，表示成功。</li></ol><p>估计很多小伙伴，一开始都会按这样的思路写代码。但如果仔细想想会发现，这段代码有问题。</p><p>有什么问题呢？</p><p>如果在高并发下，有多个请求同时查询库存，当时都大于0。由于查询库存和更新库存非原则操作，则会出现库存为负数的情况，即<code>库存超卖</code>。</p><p>当然有人可能会说，加个<code>synchronized</code>不就解决问题？</p><p>调整后代码如下：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#C792EA;">boolean</span><span style="color:#A6ACCD;"> exist </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> redisClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">query</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;">userId</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">   </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">exist</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">-</span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#A6ACCD;">   </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">   </span><span style="color:#C792EA;">synchronized</span><span style="color:#89DDFF;">(this)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">       </span><span style="color:#C792EA;">int</span><span style="color:#A6ACCD;"> stock </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> redisClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">queryStock</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productId</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">       </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">stock </span><span style="color:#89DDFF;">&lt;=</span><span style="color:#F78C6C;">0</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">         </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">0</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#A6ACCD;">       </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">       redisClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">incrby</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">-</span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">       redisClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">add</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;">userId</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">   </span><span style="color:#89DDFF;">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">;</span></span></code></pre></div><p>加<code>synchronized</code>确实能解决库存为负数问题，但是这样会导致接口性能急剧下降，每次查询都需要竞争同一把锁，显然不太合理。</p><p>为了解决上面的问题，代码优化如下：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#C792EA;">boolean</span><span style="color:#A6ACCD;"> exist </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> redisClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">query</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;">userId</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">exist</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">-</span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">redisClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">incrby</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">-</span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">)&lt;</span><span style="color:#F78C6C;">0</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">0</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">redisClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">add</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;">userId</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">;</span></span></code></pre></div><p>该代码主要流程如下：</p><ol><li>先判断该用户有没有秒杀过该商品，如果已经秒杀过，则直接返回-1。</li><li>扣减库存，判断返回值是否小于0，如果小于0，则直接返回0，表示库存不足。</li><li>如果扣减库存后，返回值大于或等于0，则将本次秒杀记录保存起来。然后返回1，表示成功。</li></ol><p>该方案咋一看，好像没问题。</p><p>但如果在高并发场景中，有多个请求同时扣减库存，大多数请求的incrby操作之后，结果都会小于0。</p><p>虽说，库存出现负数，不会出现<code>超卖的问题</code>。但由于这里是预减库存，如果负数值负的太多的话，后面万一要回退库存时，就会导致库存不准。</p><p>那么，有没有更好的方案呢？</p><h3 id="_6-3-lua脚本扣减库存" tabindex="-1">6.3 lua脚本扣减库存 <a class="header-anchor" href="#_6-3-lua脚本扣减库存" aria-label="Permalink to &quot;6.3 lua脚本扣减库存&quot;">​</a></h3><p>我们都知道lua脚本，是能够保证原子性的，它跟redis一起配合使用，能够完美解决上面的问题。</p><p>lua脚本有段非常经典的代码：</p><div class="language-lua"><button title="Copy Code" class="copy"></button><span class="lang">lua</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">StringBuilder lua </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> new </span><span style="color:#82AAFF;">StringBuilder</span><span style="color:#A6ACCD;">();</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua.</span><span style="color:#82AAFF;">append</span><span style="color:#A6ACCD;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">if (redis.call(&#39;exists&#39;, KEYS[1]) == 1) then</span><span style="color:#89DDFF;">&quot;</span><span style="color:#A6ACCD;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua.</span><span style="color:#82AAFF;">append</span><span style="color:#A6ACCD;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">    local stock = tonumber(redis.call(&#39;get&#39;, KEYS[1]));</span><span style="color:#89DDFF;">&quot;</span><span style="color:#A6ACCD;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua.</span><span style="color:#82AAFF;">append</span><span style="color:#A6ACCD;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">    if (stock == -1) then</span><span style="color:#89DDFF;">&quot;</span><span style="color:#A6ACCD;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua.</span><span style="color:#82AAFF;">append</span><span style="color:#A6ACCD;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">        return 1;</span><span style="color:#89DDFF;">&quot;</span><span style="color:#A6ACCD;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua.</span><span style="color:#82AAFF;">append</span><span style="color:#A6ACCD;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">    end;</span><span style="color:#89DDFF;">&quot;</span><span style="color:#A6ACCD;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua.</span><span style="color:#82AAFF;">append</span><span style="color:#A6ACCD;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">    if (stock &gt; 0) then</span><span style="color:#89DDFF;">&quot;</span><span style="color:#A6ACCD;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua.</span><span style="color:#82AAFF;">append</span><span style="color:#A6ACCD;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">        redis.call(&#39;incrby&#39;, KEYS[1], -1);</span><span style="color:#89DDFF;">&quot;</span><span style="color:#A6ACCD;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua.</span><span style="color:#82AAFF;">append</span><span style="color:#A6ACCD;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">        return stock;</span><span style="color:#89DDFF;">&quot;</span><span style="color:#A6ACCD;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua.</span><span style="color:#82AAFF;">append</span><span style="color:#A6ACCD;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">    end;</span><span style="color:#89DDFF;">&quot;</span><span style="color:#A6ACCD;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua.</span><span style="color:#82AAFF;">append</span><span style="color:#A6ACCD;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">    return 0;</span><span style="color:#89DDFF;">&quot;</span><span style="color:#A6ACCD;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua.</span><span style="color:#82AAFF;">append</span><span style="color:#A6ACCD;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">end;</span><span style="color:#89DDFF;">&quot;</span><span style="color:#A6ACCD;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua.</span><span style="color:#82AAFF;">append</span><span style="color:#A6ACCD;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">return -1;</span><span style="color:#89DDFF;">&quot;</span><span style="color:#A6ACCD;">);</span></span></code></pre></div><p>该代码的主要流程如下：</p><ol><li>先判断商品id是否存在，如果不存在则直接返回。</li><li>获取该商品id的库存，判断库存如果是-1，则直接返回，表示不限制库存。</li><li>如果库存大于0，则扣减库存。</li><li>如果库存等于0，是直接返回，表示库存不足。</li></ol><h2 id="_7-分布式锁" tabindex="-1">7 分布式锁 <a class="header-anchor" href="#_7-分布式锁" aria-label="Permalink to &quot;7 分布式锁&quot;">​</a></h2><p>之前我提到过，在秒杀的时候，需要先从缓存中查商品是否存在，如果不存在，则会从数据库中查商品。如果数据库中，则将该商品放入缓存中，然后返回。如果数据库中没有，则直接返回失败。</p><p>大家试想一下，如果在高并发下，有大量的请求都去查一个缓存中不存在的商品，这些请求都会直接打到数据库。数据库由于承受不住压力，而直接挂掉。</p><p>那么如何解决这个问题呢？</p><p>这就需要用redis分布式锁了。</p><h3 id="_7-1-setnx加锁" tabindex="-1">7.1 setNx加锁 <a class="header-anchor" href="#_7-1-setnx加锁" aria-label="Permalink to &quot;7.1 setNx加锁&quot;">​</a></h3><p>使用redis的分布式锁，首先想到的是<code>setNx</code>命令。</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">jedis</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">setnx</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">lockKey</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> val</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">==</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">   jedis</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">expire</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">lockKey</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> timeout</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><p>用该命令其实可以加锁，但和后面的设置超时时间是分开的，并非原子操作。</p><p>假如加锁成功了，但是设置超时时间失败了，该lockKey就变成永不失效的了。在高并发场景中，该问题会导致非常严重的后果。</p><p>那么，有没有保证原子性的加锁命令呢？</p><h3 id="_7-2-set加锁" tabindex="-1">7.2 set加锁 <a class="header-anchor" href="#_7-2-set加锁" aria-label="Permalink to &quot;7.2 set加锁&quot;">​</a></h3><p>使用redis的set命令，它可以指定多个参数。</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#C792EA;">String</span><span style="color:#A6ACCD;"> result </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> jedis</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">set</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">lockKey</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> requestId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">NX</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">PX</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> expireTime</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">OK</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">equals</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">result</span><span style="color:#89DDFF;">))</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">true;</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">false;</span></span></code></pre></div><p>其中：</p><ul><li>lockKey：锁的标识</li><li>requestId：请求id</li><li>NX：只在键不存在时，才对键进行设置操作。</li><li>PX：设置键的过期时间为 millisecond 毫秒。</li><li>expireTime：过期时间</li></ul><p>由于该命令只有一步，所以它是原子操作。</p><h3 id="_7-3-释放锁" tabindex="-1">7.3 释放锁 <a class="header-anchor" href="#_7-3-释放锁" aria-label="Permalink to &quot;7.3 释放锁&quot;">​</a></h3><p>接下来，有些朋友可能会问：在加锁时，既然已经有了lockKey锁标识，为什么要需要记录requestId呢？</p><p>答：requestId是在释放锁的时候用的。</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">jedis</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">get</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">lockKey</span><span style="color:#89DDFF;">).</span><span style="color:#82AAFF;">equals</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">requestId</span><span style="color:#89DDFF;">))</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    jedis</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">del</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">lockKey</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">true;</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">false;</span></span></code></pre></div><p>在释放锁的时候，只能释放自己加的锁，不允许释放别人加的锁。</p><p>这里为什么要用requestId，用userId不行吗？</p><p>答：如果用userId的话，假设本次请求流程走完了，准备删除锁。此时，巧合锁到了过期时间失效了。而另外一个请求，巧合使用的相同userId加锁，会成功。而本次请求删除锁的时候，删除的其实是别人的锁了。</p><p>当然使用lua脚本也能避免该问题：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> redis</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">call</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&#39;</span><span style="color:#C3E88D;">get</span><span style="color:#89DDFF;">&#39;</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">KEYS</span><span style="color:#89DDFF;">[</span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">])</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">==</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">ARGV</span><span style="color:#89DDFF;">[</span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">]</span><span style="color:#A6ACCD;"> then </span></span>
<span class="line"><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> redis</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">call</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&#39;</span><span style="color:#C3E88D;">del</span><span style="color:#89DDFF;">&#39;</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">KEYS</span><span style="color:#89DDFF;">[</span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">])</span><span style="color:#A6ACCD;"> </span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">else</span><span style="color:#A6ACCD;"> </span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">0</span><span style="color:#A6ACCD;"> </span></span>
<span class="line"><span style="color:#A6ACCD;">end</span></span></code></pre></div><p>它能保证查询锁是否存在和删除锁是原子操作。</p><h3 id="_7-4-自旋锁" tabindex="-1">7.4 自旋锁 <a class="header-anchor" href="#_7-4-自旋锁" aria-label="Permalink to &quot;7.4 自旋锁&quot;">​</a></h3><p>上面的加锁方法看起来好像没有问题，但如果你仔细想想，如果有1万的请求同时去竞争那把锁，可能只有一个请求是成功的，其余的9999个请求都会失败。</p><p>在秒杀场景下，会有什么问题？</p><p>答：每1万个请求，有1个成功。再1万个请求，有1个成功。如此下去，直到库存不足。这就变成均匀分布的秒杀了，跟我们想象中的不一样。</p><p>如何解决这个问题呢？</p><p>答：使用自旋锁。</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#89DDFF;font-style:italic;">try</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#C792EA;">Long</span><span style="color:#A6ACCD;"> start </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> System</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">currentTimeMillis</span><span style="color:#89DDFF;">();</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;font-style:italic;">while</span><span style="color:#89DDFF;">(true)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">      </span><span style="color:#C792EA;">String</span><span style="color:#A6ACCD;"> result </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> jedis</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">set</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">lockKey</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> requestId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">NX</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">PX</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> expireTime</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">     </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">OK</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">equals</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">result</span><span style="color:#89DDFF;">))</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">true;</span></span>
<span class="line"><span style="color:#A6ACCD;">     </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">     </span></span>
<span class="line"><span style="color:#A6ACCD;">     </span><span style="color:#C792EA;">long</span><span style="color:#A6ACCD;"> time </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> System</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">currentTimeMillis</span><span style="color:#89DDFF;">()</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">-</span><span style="color:#A6ACCD;"> start</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#A6ACCD;">      </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">time</span><span style="color:#89DDFF;">&gt;=</span><span style="color:#A6ACCD;">timeout</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">          </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">false;</span></span>
<span class="line"><span style="color:#A6ACCD;">      </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">      </span><span style="color:#89DDFF;font-style:italic;">try</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">          Thread</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">sleep</span><span style="color:#89DDFF;">(</span><span style="color:#F78C6C;">50</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">      </span><span style="color:#89DDFF;">}</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">catch</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#C792EA;">InterruptedException</span><span style="color:#A6ACCD;"> </span><span style="color:#A6ACCD;font-style:italic;">e</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">          e</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">printStackTrace</span><span style="color:#89DDFF;">();</span></span>
<span class="line"><span style="color:#A6ACCD;">      </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;"> </span></span>
<span class="line"><span style="color:#89DDFF;">}</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">finally</span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#82AAFF;">unlock</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">lockKey</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;">requestId</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;">}</span><span style="color:#A6ACCD;">  </span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">false;</span></span></code></pre></div><p>在规定的时间，比如500毫秒内，自旋不断尝试加锁，如果成功则直接返回。如果失败，则休眠50毫秒，再发起新一轮的尝试。如果到了超时时间，还未加锁成功，则直接返回失败。</p><h3 id="_7-5-redisson" tabindex="-1">7.5 redisson <a class="header-anchor" href="#_7-5-redisson" aria-label="Permalink to &quot;7.5 redisson&quot;">​</a></h3><p>除了上面的问题之外，使用redis分布式锁，还有锁竞争问题、续期问题、锁重入问题、多个redis实例加锁问题等。</p><p>这些问题使用redisson可以解决，由于篇幅的原因，在这里先保留一点悬念，有疑问的私聊给我。后面会出一个专题介绍分布式锁，敬请期待。</p><h2 id="_8-mq异步处理" tabindex="-1">8 mq异步处理 <a class="header-anchor" href="#_8-mq异步处理" aria-label="Permalink to &quot;8 mq异步处理&quot;">​</a></h2><p>我们都知道在真实的秒杀场景中，有三个核心流程：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131748885.png" alt="image-20220813174806814" style="zoom:67%;"><p>而这三个核心流程中，真正并发量大的是秒杀功能，下单和支付功能实际并发量很小。所以，我们在设计秒杀系统时，有必要把下单和支付功能从秒杀的主流程中拆分出来，特别是下单功能要做成mq异步处理的。而支付功能，比如支付宝支付，是业务场景本身保证的异步。</p><p>于是，秒杀后下单的流程变成如下：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131748273.png" alt="image-20220813174821205" style="zoom:67%;"><p>如果使用mq，需要关注以下几个问题：</p><h3 id="_8-1-消息丢失问题" tabindex="-1">8.1 消息丢失问题 <a class="header-anchor" href="#_8-1-消息丢失问题" aria-label="Permalink to &quot;8.1 消息丢失问题&quot;">​</a></h3><p>秒杀成功了，往mq发送下单消息的时候，有可能会失败。原因有很多，比如：网络问题、broker挂了、mq服务端磁盘问题等。这些情况，都可能会造成消息丢失。</p><p>那么，如何防止消息丢失呢？</p><p>答：加一张消息发送表。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131748744.png" alt="image-20220813174838641" style="zoom:67%;"><p>在生产者发送mq消息之前，先把该条消息写入消息发送表，初始状态是待处理，然后再发送mq消息。消费者消费消息时，处理完业务逻辑之后，再回调生产者的一个接口，修改消息状态为已处理。</p><p>如果生产者把消息写入消息发送表之后，再发送mq消息到mq服务端的过程中失败了，造成了消息丢失。</p><p>这时候，要如何处理呢？</p><p>答：使用job，增加重试机制。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131748163.png" alt="image-20220813174855099" style="zoom:67%;"><p>用job每隔一段时间去查询消息发送表中状态为待处理的数据，然后重新发送mq消息。</p><h3 id="_8-2-重复消费问题" tabindex="-1">8.2 重复消费问题 <a class="header-anchor" href="#_8-2-重复消费问题" aria-label="Permalink to &quot;8.2 重复消费问题&quot;">​</a></h3><p>本来消费者消费消息时，在ack应答的时候，如果网络超时，本身就可能会消费重复的消息。但由于消息发送者增加了重试机制，会导致消费者重复消息的概率增大。</p><p>那么，如何解决重复消息问题呢？</p><p>答：加一张消息处理表。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131749083.png" alt="image-20220813174910979" style="zoom:50%;"><p>消费者读到消息之后，先判断一下消息处理表，是否存在该消息，如果存在，表示是重复消费，则直接返回。如果不存在，则进行下单操作，接着将该消息写入消息处理表中，再返回。</p><p>有个比较关键的点是：下单和写消息处理表，要放在同一个事务中，保证原子操作。</p><h3 id="_8-3-垃圾消息问题" tabindex="-1">8.3 垃圾消息问题 <a class="header-anchor" href="#_8-3-垃圾消息问题" aria-label="Permalink to &quot;8.3 垃圾消息问题&quot;">​</a></h3><p>这套方案表面上看起来没有问题，但如果出现了消息消费失败的情况。比如：由于某些原因，消息消费者下单一直失败，一直不能回调状态变更接口，这样job会不停的重试发消息。最后，会产生大量的垃圾消息。</p><p>那么，如何解决这个问题呢？</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131749018.png" alt="image-20220813174937944" style="zoom:67%;"><p>每次在job重试时，需要先判断一下消息发送表中该消息的发送次数是否达到最大限制，如果达到了，则直接返回。如果没有达到，则将次数加1，然后发送消息。</p><p>这样如果出现异常，只会产生少量的垃圾消息，不会影响到正常的业务。</p><h3 id="_8-4-延迟消费问题" tabindex="-1">8.4 延迟消费问题 <a class="header-anchor" href="#_8-4-延迟消费问题" aria-label="Permalink to &quot;8.4 延迟消费问题&quot;">​</a></h3><p>通常情况下，如果用户秒杀成功了，下单之后，在15分钟之内还未完成支付的话，该订单会被自动取消，回退库存。</p><p>那么，在15分钟内未完成支付，订单被自动取消的功能，要如何实现呢？</p><p>我们首先想到的可能是job，因为它比较简单。</p><p>但job有个问题，需要每隔一段时间处理一次，实时性不太好。</p><p>还有更好的方案？</p><p>答：使用延迟队列。</p><p>我们都知道rocketmq，自带了延迟队列的功能。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131749999.png" alt="image-20220813174955916" style="zoom:67%;"><p>下单时消息生产者会先生成订单，此时状态为待支付，然后会向延迟队列中发一条消息。达到了延迟时间，消息消费者读取消息之后，会查询该订单的状态是否为待支付。如果是待支付状态，则会更新订单状态为取消状态。如果不是待支付状态，说明该订单已经支付过了，则直接返回。</p><p>还有个关键点，用户完成支付之后，会修改订单状态为已支付。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131750249.png" alt="image-20220813175013172" style="zoom:50%;"><h2 id="_9-如何限流" tabindex="-1">9 如何限流？ <a class="header-anchor" href="#_9-如何限流" aria-label="Permalink to &quot;9 如何限流？&quot;">​</a></h2><p>通过秒杀活动，如果我们运气爆棚，可能会用非常低的价格买到不错的商品（这种概率堪比买福利彩票中大奖）。</p><p>但有些高手，并不会像我们一样老老实实，通过秒杀页面点击秒杀按钮，抢购商品。他们可能在自己的服务器上，模拟正常用户登录系统，跳过秒杀页面，直接调用秒杀接口。</p><p>如果是我们手动操作，一般情况下，一秒钟只能点击一次秒杀按钮。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131750731.png" alt="image-20220813175040615" style="zoom:67%;"><p>但是如果是服务器，一秒钟可以请求成上千接口。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131750379.png" alt="image-20220813175055277" style="zoom:50%;"><p>这种差距实在太明显了，如果不做任何限制，绝大部分商品可能是被机器抢到，而非正常的用户，有点不太公平。</p><p>所以，我们有必要识别这些非法请求，做一些限制。那么，我们该如何现在这些非法请求呢？</p><p>目前有两种常用的限流方式：</p><ol><li>基于nginx限流</li><li>基于redis限流</li></ol><h3 id="_9-1-对同一用户限流" tabindex="-1">9.1 对同一用户限流 <a class="header-anchor" href="#_9-1-对同一用户限流" aria-label="Permalink to &quot;9.1 对同一用户限流&quot;">​</a></h3><p>为了防止某个用户，请求接口次数过于频繁，可以只针对该用户做限制。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131751018.png" alt="image-20220813175126918" style="zoom:50%;"><p>限制同一个用户id，比如每分钟只能请求5次接口。</p><h3 id="_9-2-对同一ip限流" tabindex="-1">9.2 对同一ip限流 <a class="header-anchor" href="#_9-2-对同一ip限流" aria-label="Permalink to &quot;9.2 对同一ip限流&quot;">​</a></h3><p>有时候只对某个用户限流是不够的，有些高手可以模拟多个用户请求，这种nginx就没法识别了。</p><p>这时需要加同一ip限流功能。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131751408.png" alt="image-20220813175144310" style="zoom:50%;"><p>限制同一个ip，比如每分钟只能请求5次接口。</p><p>但这种限流方式可能会有误杀的情况，比如同一个公司或网吧的出口ip是相同的，如果里面有多个正常用户同时发起请求，有些用户可能会被限制住。</p><h3 id="_9-3-对接口限流" tabindex="-1">9.3 对接口限流 <a class="header-anchor" href="#_9-3-对接口限流" aria-label="Permalink to &quot;9.3 对接口限流&quot;">​</a></h3><p>别以为限制了用户和ip就万事大吉，有些高手甚至可以使用代理，每次都请求都换一个ip。</p><p>这时可以限制请求的接口总次数。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131755978.png" alt="image-20220813175520877" style="zoom:50%;"><p>在高并发场景下，这种限制对于系统的稳定性是非常有必要的。但可能由于有些非法请求次数太多，达到了该接口的请求上限，而影响其他的正常用户访问该接口。看起来有点得不偿失。</p><h3 id="_9-4-加验证码" tabindex="-1">9.4 加验证码 <a class="header-anchor" href="#_9-4-加验证码" aria-label="Permalink to &quot;9.4 加验证码&quot;">​</a></h3><p>相对于上面三种方式，加验证码的方式可能更精准一些，同样能限制用户的访问频次，但好处是不会存在误杀的情况。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.8.30/202208131755175.png" alt="image-20220813175543071" style="zoom:50%;"><p>通常情况下，用户在请求之前，需要先输入验证码。用户发起请求之后，服务端会去校验该验证码是否正确。只有正确才允许进行下一步操作，否则直接返回，并且提示验证码错误。</p><p>此外，验证码一般是一次性的，同一个验证码只允许使用一次，不允许重复使用。</p><p>普通验证码，由于生成的数字或者图案比较简单，可能会被破解。优点是生成速度比较快，缺点是有安全隐患。</p><p>还有一个验证码叫做：<code>移动滑块</code>，它生成速度比较慢，但比较安全，是目前各大互联网公司的首选。</p><h3 id="_9-5-提高业务门槛" tabindex="-1">9.5 提高业务门槛 <a class="header-anchor" href="#_9-5-提高业务门槛" aria-label="Permalink to &quot;9.5 提高业务门槛&quot;">​</a></h3><p>上面说的加验证码虽然可以限制非法用户请求，但是有些影响用户体验。用户点击秒杀按钮前，还要先输入验证码，流程显得有点繁琐，秒杀功能的流程不是应该越简单越好吗？</p><p>其实，有时候达到某个目的，不一定非要通过技术手段，通过业务手段也一样。</p><p>12306刚开始的时候，全国人民都在同一时刻抢火车票，由于并发量太大，系统经常挂。后来，重构优化之后，将购买周期放长了，可以提前20天购买火车票，并且可以在9点、10、11点、12点等整点购买火车票。调整业务之后（当然技术也有很多调整），将之前集中的请求，分散开了，一下子降低了用户并发量。</p><p>回到这里，我们通过提高业务门槛，比如只有会员才能参与秒杀活动，普通注册用户没有权限。或者，只有等级到达3级以上的普通用户，才有资格参加该活动。</p><p>这样简单的提高一点门槛，即使是黄牛党也束手无策，他们总不可能为了参加一次秒杀活动，还另外花钱充值会员吧？</p><h1 id="图解-聊聊「秒杀」" tabindex="-1">图解 | 聊聊「秒杀」 <a class="header-anchor" href="#图解-聊聊「秒杀」" aria-label="Permalink to &quot;图解 | 聊聊「秒杀」&quot;">​</a></h1><h2 id="需求分析" tabindex="-1">需求分析 <a class="header-anchor" href="#需求分析" aria-label="Permalink to &quot;需求分析&quot;">​</a></h2><blockquote><p>“秒杀”这个词在电商行业中出现的频率较高，如京东或者淘宝平台的各种“秒杀”活动，最典型的就是“双11抢购”。</p></blockquote><blockquote><p>“秒杀”是指在有限的时间内对有限的商品数量进行抢购的一种行为，这是商家以“低价量少”的商品来获取用户的一种营销手段。</p></blockquote><h3 id="_1-功能性需求" tabindex="-1">1 功能性需求 <a class="header-anchor" href="#_1-功能性需求" aria-label="Permalink to &quot;1 功能性需求&quot;">​</a></h3><p>其实，整个秒杀的业务场景并不复杂，可即查看参与秒杀的商品信息，加上购买和支付的动作，如下图所示。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202212171102561.png" alt="image-20221217110208464" style="zoom:50%;"><h3 id="_2-最大挑战" tabindex="-1">2 最大挑战 <a class="header-anchor" href="#_2-最大挑战" aria-label="Permalink to &quot;2 最大挑战&quot;">​</a></h3><p><strong>秒杀业务最大的挑战在于3点：</strong></p><blockquote><ul><li>**瞬时：**持续时间极短，对于热门且具备极强竞争力的商品通常只有一秒。</li><li>**流量巨大：**因为价格低廉，商品性价比高，而且正常买是需要很高的价格，所以才会吸引大量的用户来争抢。</li><li>**数量有限：**因为商品的低价且性价比高，所以只有很有限的商品数量参与秒杀。</li></ul></blockquote><p>同时，在保证高并发流量承接的前提下，为了增强用户的体验和活动规则的公平性，以及防止遭到恶意破坏等，特此增加如下需求：</p><blockquote><p>（1）用户在秒杀页面无需一直刷新“抢购”按钮，待秒杀活动开始时，按钮自动点亮。</p><p>（2）在公平以及防止恶意破坏的原则下，在下单之前增加验证码的录入，或者答题的相关环节。</p><p>（3）库存不能出现问题，即不多扣也不少扣。</p><p>（4）整个秒杀活动过程持续10分钟。</p></blockquote><h3 id="性能指标预估" tabindex="-1">性能指标预估 <a class="header-anchor" href="#性能指标预估" aria-label="Permalink to &quot;性能指标预估&quot;">​</a></h3><p>通过秒杀的需求描述可得出，当前秒杀活动主要需要预估三块的性能指标：存储容量、并发量、网络带宽。</p><h4 id="_1-存储容量" tabindex="-1">1 存储容量 <a class="header-anchor" href="#_1-存储容量" aria-label="Permalink to &quot;1 存储容量&quot;">​</a></h4><blockquote><p>由于是秒杀活动，且参与的商品基本都是低价高性价比的，数量是非常有限的。所以，在订单存储上基本不用去过多考虑。</p></blockquote><h4 id="_2-并发量" tabindex="-1">2 并发量 <a class="header-anchor" href="#_2-并发量" aria-label="Permalink to &quot;2 并发量&quot;">​</a></h4><blockquote><p>针对5000万用户平均每人访问2次，则并发量为每秒16.7万左右（5000w<em>2/10</em>60）,在预留一部分，可以预估到每秒25万左右（也可以进行double下）。</p></blockquote><h4 id="_3-网络带宽" tabindex="-1">3 网络带宽 <a class="header-anchor" href="#_3-网络带宽" aria-label="Permalink to &quot;3 网络带宽&quot;">​</a></h4><blockquote><p>在带宽方面，需要进行相关优化，采取数据传输越少越好，假设单条传输在0.5KB，则根据并发量预估网络带宽为：977Mb左右（25w<em>0.5KB=122MB</em>8bit=977Mb）。</p></blockquote><h3 id="非功能性需求" tabindex="-1">非功能性需求 <a class="header-anchor" href="#非功能性需求" aria-label="Permalink to &quot;非功能性需求&quot;">​</a></h3><p>做任何系统都要考虑非功能性需求，特别是公司的核心系统，<strong>当前秒杀业务系统非功能性需求主要体现在如下几点：</strong></p><blockquote><ul><li><strong>高可用</strong>，在秒杀活动的整个持续期间内，都能对用户提供服务。</li><li><strong>高性能</strong>，让每个用户都能感受到极快的秒杀响应，不能出现大批量用户延迟较高的现象。</li><li><strong>可扩展</strong>，当流量比预期更高时，有平滑扩展的策略（也有部分产品设计成友好的拒绝策略）。</li></ul></blockquote><h2 id="概要设计" tabindex="-1">概要设计 <a class="header-anchor" href="#概要设计" aria-label="Permalink to &quot;概要设计&quot;">​</a></h2><p>通过对秒杀业务的本身认知以及上面提到的秒杀业务需求，<strong>本次秒杀系统需要着重设计如下几点：</strong></p><blockquote><p>动静分离：如何保证用户在不刷新页面的情况下，依然能进行秒杀相关数据的获取且不会耽误秒杀活动的开始。</p><p>流量分层，针对巨大流量，如何进行有效的防控，以免造成后台服务的不堪重负，以及如何避免前端页面的卡死。</p><p>高可用：如何确保后台持续提供服务。</p><p>扣减库存：如何有效扣减库存。</p></blockquote><h3 id="_1-动静分离" tabindex="-1">1 动静分离 <a class="header-anchor" href="#_1-动静分离" aria-label="Permalink to &quot;1 动静分离&quot;">​</a></h3><blockquote><p>动静分离是指，将静态页面与动态页面（或者静态数据与动态数据）解耦分离，用不同系统承载对应流量。这样可以提升整个服务访问性能和可维护性。</p></blockquote><blockquote><p>商品秒杀页面的静态数据以及动态数据，均是不同的地方提供，如下图所示。</p></blockquote><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202212171110524.png" alt="image-20221217111028461" style="zoom:80%;"><p>静态数据是指，页面中几乎不怎么变化的数据（即不依据用户的Cookie、基本信息、地域，及时间等各种属性来生成的数据），例如：</p><blockquote><ul><li>CSS和JavaScript中的静态文件。</li><li>活动页中的HTML静态文件。</li><li>图片等相关资源文件。</li><li>其他与用户信息无关的静态数据。</li></ul></blockquote><blockquote><p>对于这种分离出来的静态数据可以进行缓存。在缓存之后，这些静态数据的访问效率就提高了，系统也更快了。可以使用代理服务器进行静态数据的缓存。</p></blockquote><blockquote><p>动态数据是指，依据当前用户属性动态生成的数据，在浏览淘宝首页时，每个用户所看到的商品都是不一样的，这就是淘宝的“千人千面”——针对不同用户做不同的推荐；在百度搜索中是依据不同用户的输入条件，以及用户的习惯给出不同的结果页。这其中的数据就是动态数据。</p></blockquote><h3 id="_2-流量分层" tabindex="-1">2 流量分层 <a class="header-anchor" href="#_2-流量分层" aria-label="Permalink to &quot;2 流量分层&quot;">​</a></h3><blockquote><p>在“秒杀”业务中，商品价格具有强大的吸引力，所以会受到很多用户的关注，但是商品数量是有限的。所以，在千万的用户中可能只有100人能得到商品，对于系统来说，有90%以上的流量属于无效流量。</p></blockquote><blockquote><p>“秒杀”业务希望有大量的用户来关注“秒杀”活动，但是在用户真正下单时又不能将这些流量全部放过，所以，需要设计一套高效的流量管控方案，来有效地控制请求流量，过滤掉没必要的流量。</p></blockquote><blockquote><p>对于瞬时流量洪峰可以采用倒三角的分层级逐层控制方式，共分为CDN、反向代理（Nginx）、后端服务及DB这四个层级。接下来，就来看看每一层级是怎么控制流量的，如下图所示。</p></blockquote><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202212171126130.png" alt="image-20221217112613050" style="zoom:50%;"><h3 id="_3-高可用" tabindex="-1">3 高可用 <a class="header-anchor" href="#_3-高可用" aria-label="Permalink to &quot;3 高可用&quot;">​</a></h3><blockquote><p>要想在整个“秒杀”活动持续期间内，依然能对用户提供良好的体验，则秒杀系统架构在设计时不能设计成单节点的架构。</p></blockquote><blockquote><p>单节点是所有系统设计中的大忌，因为单节点系统意味着系统的不稳定性较高，可能会出现不可用的情况，会给企业带来直接的损失。在系统设计（特别是“秒杀”这类对高并发要求极高的系统）时，必须保证系统的高可用，如下图所示。</p></blockquote><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202212171126326.png" alt="image-20221217112650248" style="zoom:50%;"><h3 id="_4-扣减库存" tabindex="-1">4 扣减库存 <a class="header-anchor" href="#_4-扣减库存" aria-label="Permalink to &quot;4 扣减库存&quot;">​</a></h3><blockquote><p>对于“秒杀”活动，通常，公司是不允许商品超卖（即下单成功的数量不能大于商品存存数量）的。一旦超卖，则会给公司造成损失。如果被恶意流量利用，则损失是巨大的。</p></blockquote><blockquote><p>库存对于电商平台来说是一个重要的业务指标，所以在技术上需要合理设计扣减库存，不能出现“超卖”现象。通常，扣减库存常有以下3种方式：</p></blockquote><blockquote><ul><li>下单扣库存：在用户下单后就扣减库存。</li><li>支付扣库存：用户付完款后再扣减库存。</li><li>预扣库存：在用户下完订单后，系统会为其锁定库存一段时间，在超过锁定时间后会自动释放锁定的库存。</li></ul></blockquote><h3 id="_5-系统架构设计⭐" tabindex="-1">5 系统架构设计⭐ <a class="header-anchor" href="#_5-系统架构设计⭐" aria-label="Permalink to &quot;5 系统架构设计⭐&quot;">​</a></h3><p>根据上面讨论，针对当前秒杀架构如下图所示。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202212171131491.png" alt="image-20221217113058922" style="zoom:50%;"><p><strong>如上架构比较简洁，主要分为以下5层。</strong></p><blockquote><ul><li>用户层：用户端的展现部分，主要涉及商品的相关信息及当前“秒杀”活动的信息。</li><li>CDN层：缓存“秒杀”活动的静态资源文件。</li><li>负载均衡层：拦截请求及分发路由等。</li><li>服务层：“秒杀”活动的具体交易的相关逻辑处理。</li><li>基础设施层：数据存储、大数据计算及消息推送相关操作。</li></ul></blockquote><p>其部署架构图如下：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202212171132519.png" alt="image-20221217113243446" style="zoom:80%;"><h2 id="详细设计" tabindex="-1">详细设计 <a class="header-anchor" href="#详细设计" aria-label="Permalink to &quot;详细设计&quot;">​</a></h2><h3 id="_1-动静分离设计" tabindex="-1">1 动静分离设计 <a class="header-anchor" href="#_1-动静分离设计" aria-label="Permalink to &quot;1 动静分离设计&quot;">​</a></h3><p>实施动静分离架构可以采用“分而治之”的办法，即将动态数据和静态数据解耦，分别使用各自的架构系统来承载对应的流量：</p><blockquote><ul><li>对于静态数据，推荐缩短用户请求路径，因为路径越短，访问速度也就越快。另外，即尽可能将静态数据缓存起来。</li><li>对于动态数据，一般用户端需要和服务端进行交互才能获取，所以，请求路径较长，访问速度会慢一点。下图展示了动静分离方案。</li></ul></blockquote><blockquote><p>静态数据访问速度很快，而动态数据访问速度较慢。那么试想下，可以将需要动态获取的数据给提前生成好，然后使用静态页面加速技术来访问吗？如果这样可以，那动态数据访问的速度就变快了。</p></blockquote><blockquote><p>这样是可以的，需要用到比较流行的“页面静态化”技术。页面静态化技术是指，直接缓存HTTP连接，而不仅是缓存数据。如下图所示，代理服务器根据请求的URL直接将HTTP对应的响应头及响应消息体返回，流程简洁且高效。</p></blockquote><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202212171114079.png" alt="image-20221217111442005" style="zoom:67%;"><h3 id="_2-流量分层设计" tabindex="-1">2 流量分层设计 <a class="header-anchor" href="#_2-流量分层设计" aria-label="Permalink to &quot;2 流量分层设计&quot;">​</a></h3><blockquote><p>流量分层主要体现在对于CDN层、反向代理层、后端服务层以及数据层流量进行控制。</p></blockquote><h4 id="_1-cdn层流量控制" tabindex="-1">1 CDN层流量控制 <a class="header-anchor" href="#_1-cdn层流量控制" aria-label="Permalink to &quot;1 CDN层流量控制&quot;">​</a></h4><p>由动静分离技术可以想到：应尽量将尽可能多的数据提前生成，然后将其放入CDN节点缓存中（因为CDN层在物理架构上离用户比较近）。</p><p>所以，如果绝大部分的流量都在这一层获取数据，则到达后端的流量会减少很多，如下图所示。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202212171136462.png" alt="image-20221217113630407" style="zoom:50%;"><h4 id="_2-反向代理层流量控制" tabindex="-1">2 反向代理层流量控制 <a class="header-anchor" href="#_2-反向代理层流量控制" aria-label="Permalink to &quot;2 反向代理层流量控制&quot;">​</a></h4><p>在动静分离方案中，讲到通过“页面静态化技术”加速动态数据的获取，即提前将动态数据生成好，然后对其进行静态化处理。</p><p>所以，这里就可以依据页面静态化加速技术，通过后端服务Job的方式定时提前生成前端需要静态的数据；然后，将其发送到内容分发服务上；最后，分发服务会将这些静态化页面数据分发到所有的反向代理服务器上，如下图所示。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202212171136970.png" alt="image-20221217113608918" style="zoom:80%;"><p>在“秒杀”业务中，活动详情页上有一个倒计时的模块，用户可以看到当前“秒杀”活动还剩余多少时间开始。</p><p>这种逻辑简单的功能可以直接使用Nginx来实现：利用nginx-lua插件，使用lua脚本获取当前Nginx服务器的时间进行计算倒计时。另外，商品库存数据也可以通过Nginx直接访问分布式缓存来获取，如下图所示。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202212171135474.png" alt="image-20221217113550414" style="zoom:80%;"><p>“秒杀”业务中的商品价格很低，对于用户有很大的吸引力，所以可能会有人利用“秒杀器”进行不公平竞争，且有可能存在竞争对手恶意刷请求的情况。</p><p>如果存在这样的情况，那本次活动就是有风险的，万一被恶意流量独占了库存，则会导致正常用户不能抢购商品，也有可能这种恶意的请求会对后端系统造成严重冲击，甚至造成后端系统瘫痪。</p><p>对于这种恶意请求，最好有一套机制能提前感知，并将恶意请求提前封存。可以在Nginx层中控制；也可以在Nginx中配置用户的访问频率（例如每分钟只能访问10次）；还可以使用Lua脚本编写一些简单业务逻辑的接口，例如，通过调用接口直接封掉指定IP地址或UserAgent的请求。</p><h4 id="_3-后端服务层流量控制" tabindex="-1">3 后端服务层流量控制 <a class="header-anchor" href="#_3-后端服务层流量控制" aria-label="Permalink to &quot;3 后端服务层流量控制&quot;">​</a></h4><p>对于服务层的流量控制，有以下几点建议：</p><blockquote><ul><li>在程序开发上，代码独立，不要与平台其他项目一起。</li><li>在部署时，应用独立部署，分散流量，避免不合适的流量影响主体业务。</li><li>使用独立域名，或者按照一定的URL规则在反向代理层进行路由。</li><li>做好系统保护和限流，进一步减少不必要的流量。</li></ul></blockquote><blockquote><p>当“到达系统中的请求数”明显大于“系统能够处理的最大请求数”时，可以直接拒绝这些多余的请求，直接返回“秒杀”活动结束的信息。例如，活动开始时的商品库存是100，目前库存只剩50了，如果“每台服务器待处理的请求数”已经超过“商品总库存数（100）”了，则可以直接终止掉多余的请求。</p></blockquote><h4 id="_4-数据库层流量控制" tabindex="-1">4 数据库层流量控制 <a class="header-anchor" href="#_4-数据库层流量控制" aria-label="Permalink to &quot;4 数据库层流量控制&quot;">​</a></h4><p>对于请求到数据中的流量，写入的流量就是真正下单成功的流量，即需要扣减库存的动作。有如下建议：</p><blockquote><ul><li>如果不是临时的活动，则建议使用独立的数据库作为“秒杀”活动的数据库。</li><li>将数据库配置成读写分离。</li><li>尝试去除行锁。</li></ul></blockquote><p>对于数据库行锁的优化，可以通过将商品进行拆分来实现——增加ID，如下图所示。对于单一的“秒杀”活动这会得到显著效果。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202212171134217.png" alt="image-20221217113453143" style="zoom:50%;"><p>从流量分层控制方案可看出，瞬时流量就像被漏斗过滤了似的，应尽量将数据和请求量一层一层地过滤掉。这种流量分层控制核心思想：在不同的层级中尽可能地过滤掉无效的请求，到达“倒三角”最末端的请求才是有效的请求。</p><h4 id="_5-高可用" tabindex="-1">5 高可用 <a class="header-anchor" href="#_5-高可用" aria-label="Permalink to &quot;5 高可用&quot;">​</a></h4><blockquote><p>在系统设计时想要做到高可用，避免单节点的一个小妙招：将服务无状态化。如果无法完全无状态化（如存储系统），则可以通过冗余多个备份节点的方案来避免单节点。</p></blockquote><blockquote><p>由于篇幅原因，高可用此处就不再赘述，大家可以查看**《高并发系统实战派》**一书里面针对高并发系统的真实设计案例，毫无保留的分享出了企业级高并发系统实战。</p></blockquote><h3 id="_3-扣减库存设计" tabindex="-1">3 扣减库存设计 <a class="header-anchor" href="#_3-扣减库存设计" aria-label="Permalink to &quot;3 扣减库存设计&quot;">​</a></h3><blockquote><p>由于在“秒杀”场景中商品一般优惠力度很大，对用户很具有吸引力，所以，在这种场景中使用“下单扣库存”方式更为合适。</p></blockquote><blockquote><p>在“秒杀”场景中，大部分用户抱着“抢到就是赚到”的想法，基本都会去付款的，但如果真有竞争对手恶意下单不付款，那我们该怎么办？前面在流量管控中已经说到，可以对请求日志进行实时分析，让风控系统选择出恶意用户，然后将其封停。</p></blockquote><blockquote><p>在“秒杀”场景中，通过流量分层控制可以分层管控大量的“读”请求。但是，依然会有很大的流量进入真正的下单逻辑。对于这么大的流量，除前面说的数据库隔离外，还需要进一步优化库存，否则数据库读/写依然是系统的瓶颈。</p></blockquote><p>接下来看看如何优化大流量“秒杀”场景中的库存数量扣减操作。</p><h4 id="_1-利用缓存技术" tabindex="-1">1 利用缓存技术 <a class="header-anchor" href="#_1-利用缓存技术" aria-label="Permalink to &quot;1 利用缓存技术&quot;">​</a></h4><blockquote><p>在“秒杀”场景中，如果只是一个扣减库存数量这样的简单流程，则可以先将库存数量直接放在缓存中，然后用分布式缓存（如Redis）的超高性能去应对这种瞬时流量洪峰下的系统挑战。</p></blockquote><blockquote><p>使用缓存是存在一定风险的，比如，缓存节点出现了异常，那库存数量该怎么算？</p></blockquote><blockquote><p>使用缓存，不仅要考虑分布式缓存高可用（如何设计可以查看我的新书“高并发系统实战派”），还要考虑各种限流容错机制，以确保分布式缓存对外提供服务。</p></blockquote><h4 id="_2-异步处理技术" tabindex="-1">2 异步处理技术 <a class="header-anchor" href="#_2-异步处理技术" aria-label="Permalink to &quot;2 异步处理技术&quot;">​</a></h4><blockquote><p>如果是复杂的扣减库存（如涉及商品信息本身或牵连其他系统），则建议使用数据库进行库存数量的扣减，可以使用异步的方式来应对这种高并发的库存的更新。</p></blockquote><blockquote><p>①在用户下单时，不立刻生成订单，而是将所有订单依次放入队列。</p><p>②下单模块依据自身的处理速度，从队列中依次获取订单进行“下单扣库存”操作。</p><p>③在订单生成成功后，用户即可进行支付操作了。</p></blockquote><blockquote><p>这种方式是针对“秒杀”场景的，依据“先到先得”原则来保证公平公正，所有用户都可以抢购，然后等待订单处理，最后生成订单（如果库存不足，则生成订单失败）。</p></blockquote><p>这样的逻辑，对用户来说体验不是很差。具体排队逻辑如下图所示。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202212171124698.png" alt="image-20221217112432646" style="zoom:80%;"><h2 id="搭建千万级流量秒杀系统需要哪些技术" tabindex="-1">搭建千万级流量秒杀系统需要哪些技术 <a class="header-anchor" href="#搭建千万级流量秒杀系统需要哪些技术" aria-label="Permalink to &quot;搭建千万级流量秒杀系统需要哪些技术&quot;">​</a></h2><p>前面介绍了千万级流量“秒杀”系统的基本架构、“秒杀”系统的设计原则、如何做动静分离方案和流量控制，以及扣减库存方面内容。这些都是设计高并发“秒杀”系统必须要考虑的。</p><p>“秒杀”系统的流程并不复杂——只是一个“下单扣库存”的动作，但由于其独特的业务特点，所以在进行系统设计时不能大意。对于瞬时流量洪峰的高并发“秒杀”系统，我们需要什么技术呢？下面来总结一下。</p><h3 id="_1-数据的静态化的技术" tabindex="-1">1 数据的静态化的技术 <a class="header-anchor" href="#_1-数据的静态化的技术" aria-label="Permalink to &quot;1 数据的静态化的技术&quot;">​</a></h3><blockquote><p>用来应对高并发读的请求，主要涉及以下内容，这些在**《高并发系统实战派》**一书中详细分享了真实使用场景已经技术方案：各层级缓存的处理（即多级缓存的技术） 分布式缓存技术</p></blockquote><h3 id="_2-负载均衡反向代理技术" tabindex="-1">2 负载均衡反向代理技术 <a class="header-anchor" href="#_2-负载均衡反向代理技术" aria-label="Permalink to &quot;2 负载均衡反向代理技术&quot;">​</a></h3><blockquote><ul><li>LVS</li><li>Nginx</li></ul></blockquote><h3 id="_3-异步处理技术" tabindex="-1">3 异步处理技术 <a class="header-anchor" href="#_3-异步处理技术" aria-label="Permalink to &quot;3 异步处理技术&quot;">​</a></h3><blockquote><ul><li>消息队列技术</li><li>排队系统技术</li></ul></blockquote><h3 id="_4-系统架构设计技术" tabindex="-1">4 系统架构设计技术 <a class="header-anchor" href="#_4-系统架构设计技术" aria-label="Permalink to &quot;4 系统架构设计技术&quot;">​</a></h3><blockquote><ul><li>系统模块化划分</li><li>微服务架构思想</li></ul></blockquote><h3 id="_5-系统监控技术" tabindex="-1">5 系统监控技术 <a class="header-anchor" href="#_5-系统监控技术" aria-label="Permalink to &quot;5 系统监控技术&quot;">​</a></h3><blockquote><ul><li>日志监控</li><li>服务监控</li></ul></blockquote><h1 id="如何设计秒杀系统" tabindex="-1">如何设计秒杀系统 <a class="header-anchor" href="#如何设计秒杀系统" aria-label="Permalink to &quot;如何设计秒杀系统&quot;">​</a></h1><p>秒杀系统的设计是高级职位面试中非常高频的一道题目，它可以较好地考察候选人的知识体系情况。对于我们来说，学习秒杀系统的设计，能够让我们学以致用，设计系统的时候考虑得更加全面。今天就带你一起来看看怎么设计一个秒杀系统！</p><img src="https://mmbiz.qpic.cn/mmbiz_png/AVWicyZuuClH38oJYSMwnsM8LtcCsMfyEHIWJ15LgIbuduKK6oBZRIpuOpb9EeRLgNb0AfAeIiauJG2via5eCkOwA/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片" style="zoom:80%;"><blockquote><p>活动一般出现在电商的促销活动中，一般是指定了很少数量的商品，以极低的价格，让大量的用户参与，从而造成大量用户在极短的时间内参与活动，进而造成系统在极短的时间内有极高的流量。系统设计的目的是使系统能够稳定地支撑活动的进行，因此其稳定性、高可用是我们考虑的第一位。</p></blockquote><blockquote><p>要知道如何进行秒杀系统的优化，那我们需要先对请求的整个流程有个全局的认识。<strong>一般来说，秒杀活动请求以公网为划分点，可以分为：前端部分、后端部分。</strong> 前端部分指的是从用户端到进入后端服务前的部分，包括了移动端的处理、DNS 解析、公网的数据传递等。</p></blockquote><blockquote><p>后端部分指的是经公网进入了后端的服务器网络里，包括了前置的负载均衡（Nginx 等）、应用服务器、数据库层等。秒杀活动的整个流程可以用下图来表示。</p></blockquote><p><img src="https://mmbiz.qpic.cn/mmbiz_png/AVWicyZuuClH38oJYSMwnsM8LtcCsMfyET1k58MJzSHdYtoXMOq12ib8ice3vPJC2WauMTa8oicOSrGxpQTQ5FibbRg/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片">the-process-of-network-request</p><blockquote><p>我们要去设计一个秒杀系统，那自然也是从这两大部分来进行优化。整体思路是尽量将流量挡在前面，让尽量少的流量留到后端部分。因为越往后端，我们的处理逻辑就越重，其处理能力也越弱。</p></blockquote><h2 id="前端优化" tabindex="-1">前端优化 <a class="header-anchor" href="#前端优化" aria-label="Permalink to &quot;前端优化&quot;">​</a></h2><p>对于前端部分来说，常见的优化手段有：页面静态化 + CDN、请求频率限制。</p><h3 id="页面静态化-cdn" tabindex="-1">页面静态化 + CDN <a class="header-anchor" href="#页面静态化-cdn" aria-label="Permalink to &quot;页面静态化 + CDN&quot;">​</a></h3><blockquote><p>一般来说，活动页面是流量最大的地方。活动页面上绝大部分内容都是固定的，比如：商品描述、图片等。这时候没有必要每次都去请求服务端，而是将这些静态的内容放到 CDN 上。</p></blockquote><p>每次打开页面的时候，直接去请求 CDN 服务器，能极大地减少后端的请求流量。加入了 CDN 之后，其请求过程如下：</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/AVWicyZuuClH38oJYSMwnsM8LtcCsMfyEITjRtrb2qOnBxKCDutyxLibeG2YjtC71IuzF2mZpPhbuDtWSyWgqKJQ/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p>CDN 优化静态数据</p><blockquote><p>所谓的 CDN 就是内容分发网络，它由非常多台分布在世界各地的缓存服务器组成。每次用户请求特定域名的时候，会转发到对应 CDN 的 DNS 解析服务器，随后会返回一台离用户地理位置最近的一台 CDN 服务器。随后，用户直接请求这台 CDN 服务器获取数据，从而极大地减少了长途网络传输的时间，并且也减少了后端服务器的压力。</p></blockquote><blockquote><p><strong>因此，对于秒杀活动设计来说，我们可以将所有可以静态化的内容全部静态化，然后将其配置在 CDN 服务器上。这样既提高了用户打开页面的时间，又减少了后端服务器的压力。</strong></p></blockquote><h3 id="请求频率限制" tabindex="-1">请求频率限制 <a class="header-anchor" href="#请求频率限制" aria-label="Permalink to &quot;请求频率限制&quot;">​</a></h3><p>请求频率限制，指的是根据业务的特点，在前端做一些流量拦截，减少后端服务器的压力。常见的拦截方式有：</p><blockquote><ol><li>设定一个请求概率，只允许 30% 的概率向后端发送接口请求。</li><li>设定一个请求频率，例如 10 秒钟只能请求 1 次，随后按钮置灰。</li></ol></blockquote><blockquote><p>通过这种方式，我们可以减少很大一部分流量。但在具体实现的时候，可能需要考虑安全问题，预防某些用户直接调用后台接口，绕过前端的频率检查。</p></blockquote><blockquote><p>常见的方法是在频率检查时生成一个参数，随后请求后端服务时携带上该参数。没有该参数的请求，都视为非法请求，直接拒绝该请求。</p></blockquote><h2 id="后端优化" tabindex="-1">后端优化 <a class="header-anchor" href="#后端优化" aria-label="Permalink to &quot;后端优化&quot;">​</a></h2><p>无论我们做多大的努力，始终还是会有不少流量会来到后端服务器这里。一般来说，后端的优化有如下几种方式：</p><blockquote><ol><li>增加缓存层 + 预热数据</li><li>MQ 异步处理</li><li>限流、熔断、兜底</li><li>业务侧优化</li></ol></blockquote><h3 id="增加缓存层-预热数据" tabindex="-1">增加缓存层 + 预热数据 <a class="header-anchor" href="#增加缓存层-预热数据" aria-label="Permalink to &quot;增加缓存层 + 预热数据&quot;">​</a></h3><blockquote><p>如果我们所有数据都去读取数据库，数据库可能无法承受较大的流量，此时一个常见的优化就是增加缓存层。</p></blockquote><blockquote><p>当我们需要查询数据库之前，我们先去查询缓存，这样可以减少绝大部分的数据库请求，减轻数据库压力。如果在缓存中找不到数据，我们再去请求数据库，随后再将数据缓存到缓存中。</p></blockquote><blockquote><p>在引入缓存层的时候，我们需要考虑缓存击穿、缓存穿透的可能性，在写相关代码的时候就要做好这些优化。另外，我们在秒杀活动开始之前，可以手动将热点数据加载到缓存中，从而避免秒杀时去请求数据库。</p></blockquote><h3 id="mq-异步处理" tabindex="-1">MQ 异步处理 <a class="header-anchor" href="#mq-异步处理" aria-label="Permalink to &quot;MQ 异步处理&quot;">​</a></h3><blockquote><p>我们知道秒杀活动一般涉及抢购、下单、支付、发货等阶段，而抢购与后续的几个阶段是可以异步执行的。为了避免对下单、支付、发货等阶段产生影响，我们可以将抢购阶段与后续阶段用 MQ 进行解耦处理。当用户抢购成功后，往消息队列中丢入一台消息，随后再由订单系统消费进行下单处理。</p></blockquote><blockquote><p>通过各系统之间的解耦处理，我们可以将原本同步的处理方式变为异步处理，从而大大的减少了请求的处理时间，提高了系统的并发处理能力。其次，也能避免系统之间相互影响，提高了整体系统的稳定性。</p></blockquote><h3 id="限流、熔断、降级" tabindex="-1">限流、熔断、降级 <a class="header-anchor" href="#限流、熔断、降级" aria-label="Permalink to &quot;限流、熔断、降级&quot;">​</a></h3><blockquote><p>虽然我们做了非常多的优化措施，但还是可能存在请求超量的可能性，那怎么办呢？</p></blockquote><blockquote><p>我们可以在每个业务系统做限流操作，从而避免因为请求太多，导致整个系统都无法工作。当并发请求在正常范围内时，我们正常处理请求。当超过设置的限流阈值时，我们则直接拒绝该请求，提示用户抢购失败。</p></blockquote><blockquote><p>如果没有限流操作，那么系统直接崩溃了，一个请求都处理不了。而通过限流这种方式，系统至少还可以保持正常工作，而不至于一个请求都处理不了。而超量的需求，本来就处理不了，因此提示失败也是情理之中。</p></blockquote><p>除了限流之外，不同的系统还可以采用熔断、降级的服务治理措施。</p><blockquote><p>熔断指的是请求的错误次数超过阈值时，不再到用后端服务，直接返回失败。同时每隔一定时间放几个请求去重试后端服务，看看是否正常。如果正常则关闭熔断状态，如果失败则继续快速失败。<strong>熔断的目的是避免因下游短暂的异常，导致上游不断重试，最终造成下游有太多请求，最终压垮下游系统。</strong></p></blockquote><blockquote><p>降级指的是当服务失败或异常后，返回指定的默认信息。<strong>降级的目的是保证有基本的信息，当下游异常时，与其返回空信息，不如返回一个有业务含义的默认信息，可以提高用户体验。</strong></p></blockquote><h3 id="业务侧优化" tabindex="-1">业务侧优化 <a class="header-anchor" href="#业务侧优化" aria-label="Permalink to &quot;业务侧优化&quot;">​</a></h3><blockquote><p>一般来说，经过上述的整体优化之后，系统已经能够比较稳当地应对秒杀活动了。如果此时还是流量比较大，那么或许应该从业务侧去进行优化了。</p></blockquote><blockquote><p>例如 12306 刚开始的时候，购买时间都在同一时刻，这导致同一时刻并发量太大，系统经常支撑不住。后来 12306 将购票周期放长，可以提前 20 天购买火车票。通过业务侧的优化，我们将本来在 1 个小时的抢购分摊到了 20 天，服务器压力一下子降低了 480 倍！</p></blockquote><blockquote><p><strong>张小龙也说过：如果公司最厉害的程序员来实现业务都觉得复杂，那很可能就是业务确实不合理，这时候应该从业务侧进行优化。</strong></p></blockquote><blockquote><p>例如一个存储了 10 亿条记录的消息记录表，业务侧既想查询速度快，又想进行 1 年数据范围的数据查询，这无论如何都是无法实现的。这时候就需要从业务需求侧进行优化，否则是无法两全其美的。</p></blockquote><blockquote><p>对于这个场景，一个合理的实现方式是：要实现 1 年数据范围的查询，那么只能根据消息 ID 进行，因为这样可以使用上索引。而要根据时间范围进行查询，只能缩短查询时间到 3 天内，这样也可以满足业务需求。</p></blockquote><blockquote><p><strong>因此从业务侧进行优化，是一个四两拨千斤的办法，可以极大地降低技术侧实现的难度。</strong></p></blockquote><h2 id="秒杀优化总结" tabindex="-1">秒杀优化总结 <a class="header-anchor" href="#秒杀优化总结" aria-label="Permalink to &quot;秒杀优化总结&quot;">​</a></h2><p>设计一个秒杀系统，整体而言可以从前端与后端进行优化。</p><p><strong>对于前端优化而言，可以从「页面静态化 + CDN」、请求频率限制进行优化。</strong></p><blockquote><p>其中「页面静态化 + CDN」指的是将不变的静态数据固定下来，然后放入 CDN 服务器，从而降低用户请求的响应速度，降低服务器的并发压力。请求频率限制，则是通过抢购概率与抢购频率限制，降低后端服务器的服务压力。</p></blockquote><p><strong>对于后端优化而言，一般有「增加缓存层 + 预热数据」、「MQ 异步处理」、「限流、熔断、降级」、业务侧优化这 4 种优化方式。</strong></p><blockquote><p>其中「增加缓存层 + 预热数据」指的是将热点数据存入缓存，并在活动开始前提前加载到缓存中，降低数据库层的读取压力。「MQ 异步处理」指的是对于非必要的业务逻辑，通过 MQ 进行异步处理，降低请求处理延时，同时提高业务系统整体稳定性。</p></blockquote><blockquote><p>「限流、熔断、降级」是对于整体微服务的保护，其中限流指的是对请求进行限制，当超过限流阈值时，直接拒绝请求，保护系统本身；熔断指的是保护下游系统，当请求下游系统连续错误超过阈值时，自动不去请求下游系统，避免因重试流量过大击垮下游系统。</p></blockquote><blockquote><p>降级指的是当请求失败时，自动返回默认数据，提高用户体验。业务侧优化，则是指从业务层面去进行逻辑优化，从而降低技术复杂度，使得业务与技术复杂度达到一个平衡的状态，有利于更好地实现秒杀系统的高可用与高并发。</p></blockquote><blockquote><p>上面说到的 6 个优化思路，是设计秒杀系统常见的优化思路。<strong>但在实际业务场景中，除了要保障正常的功能设计之外，还还考虑防刷、安全、黑产等问题</strong>，此时可能需要多考虑一些其他优化，例如：黄牛利用抢购工具抢购，导致正常用户无法抢到商品等。</p></blockquote><blockquote><p>这时候可能需要考虑增加验证码，用 App 设备指纹等风控措施。<strong>此外，对于秒杀系统而言，做好业务指标和系统指标的埋点监控也是非常重要的。</strong></p></blockquote><h1 id="我劝你别去阿里了-秒杀都搞不定" tabindex="-1">我劝你别去阿里了，秒杀都搞不定 <a class="header-anchor" href="#我劝你别去阿里了-秒杀都搞不定" aria-label="Permalink to &quot;我劝你别去阿里了，秒杀都搞不定&quot;">​</a></h1><p>最近有读者在询问阿里面试的一些技巧，我随便问了他几个问题，他答得很一般，主要包括海量数据处理、线上问题排查、秒杀系统设计。尤其是秒杀系统设计，他的答案确实让人不敢恭维。</p><p>如果连秒杀系统都不会，或者不熟练，那去阿里面试，基本就等同于浪费时间。如果面试不通过，人才库也有不通过的描述，会影响以后的面试。所以，我建议准备充分，珍惜阿里面试机会。</p><p>之前讨论过关于秒杀的内容，下面，我们来一起来看看秒杀场景的优化版，欢迎沟通和交流。</p><h2 id="秒杀业务分析" tabindex="-1">秒杀业务分析 <a class="header-anchor" href="#秒杀业务分析" aria-label="Permalink to &quot;秒杀业务分析&quot;">​</a></h2><p><strong>正常电子商务流程</strong></p><div class="language-"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">（1）查询商品；</span></span>
<span class="line"><span style="color:#A6ACCD;">（2）创建订单；</span></span>
<span class="line"><span style="color:#A6ACCD;">（3）扣减库存；</span></span>
<span class="line"><span style="color:#A6ACCD;">（4）更新订单；</span></span>
<span class="line"><span style="color:#A6ACCD;">（5）付款；</span></span>
<span class="line"><span style="color:#A6ACCD;">（6）卖家发货</span></span></code></pre></div><p><strong>秒杀业务的特性</strong></p><div class="language-"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">（1）低廉价格；</span></span>
<span class="line"><span style="color:#A6ACCD;">（2）大幅推广；</span></span>
<span class="line"><span style="color:#A6ACCD;">（3）瞬时售空；</span></span>
<span class="line"><span style="color:#A6ACCD;">（4）一般是定时上架；</span></span>
<span class="line"><span style="color:#A6ACCD;">（5）时间短、瞬时并发量高；</span></span></code></pre></div><h2 id="秒杀技术挑战" tabindex="-1">秒杀技术挑战 <a class="header-anchor" href="#秒杀技术挑战" aria-label="Permalink to &quot;秒杀技术挑战&quot;">​</a></h2><blockquote><p>假设某网站秒杀活动只推出一件商品，预计会吸引1万人参加活动，也就说最大并发请求数是10000，秒杀系统需要面对的技术挑战有：</p></blockquote><h3 id="对现有网站业务造成冲击" tabindex="-1">对现有网站业务造成冲击 <a class="header-anchor" href="#对现有网站业务造成冲击" aria-label="Permalink to &quot;对现有网站业务造成冲击&quot;">​</a></h3><blockquote><p>秒杀活动只是网站营销的一个附加活动，这个活动具有时间短，并发访问量大的特点，如果和网站原有应用部署在一起，必然会对现有业务造成冲击，稍有不慎可能导致整个网站瘫痪。</p></blockquote><blockquote><p><strong>解决方案</strong>：将秒杀系统独立部署，甚至<code>使用独立域名，使其与网站完全隔离</code>。</p></blockquote><h3 id="高并发下的应用、数据库负载" tabindex="-1">高并发下的应用、数据库负载 <a class="header-anchor" href="#高并发下的应用、数据库负载" aria-label="Permalink to &quot;高并发下的应用、数据库负载&quot;">​</a></h3><blockquote><p>用户在秒杀开始前，通过不停刷新浏览器页面以保证不会错过秒杀，这些请求如果按照一般的网站应用架构，访问应用服务器、连接数据库，会对应用服务器和数据库服务器造成负载压力。</p></blockquote><blockquote><p><strong>解决方案</strong>：重新设计秒杀商品页面，不使用网站原来的商品详细页面，<code>页面内容静态化，用户请求不需要经过应用服务</code>。</p></blockquote><h3 id="突然增加的网络及服务器带宽" tabindex="-1">突然增加的网络及服务器带宽 <a class="header-anchor" href="#突然增加的网络及服务器带宽" aria-label="Permalink to &quot;突然增加的网络及服务器带宽&quot;">​</a></h3><blockquote><p>假设商品页面大小200K（主要是商品图片大小），那么需要的网络和服务器带宽是2G（200K×10000），这些网络带宽是因为秒杀活动新增的，超过网站平时使用的带宽。</p></blockquote><blockquote><p><strong>解决方案</strong>：因为秒杀新增的网络带宽，必须和运营商重新购买或者租借。为了减轻网站服务器的压力，<strong><code>需要将秒杀商品页面缓存在CDN，同样需要和CDN服务商临时租借新增的出口带宽</code></strong>。</p></blockquote><h3 id="直接下单" tabindex="-1">直接下单 <a class="header-anchor" href="#直接下单" aria-label="Permalink to &quot;直接下单&quot;">​</a></h3><blockquote><p>秒杀的游戏规则是到了秒杀才能开始对商品下单购买，在此时间点之前，只能浏览商品信息，不能下单。而下单页面也是一个普通的URL，如果得到这个URL，不用等到秒杀开始就可以下单了。</p></blockquote><blockquote><p><strong>解决方案</strong>：为了避免用户直接访问下单页面URL，需要将改URL动态化，即使秒杀系统的开发者也无法在秒杀开始前访问下单页面的URL。是在<code>下单页面URL加入由服务器端生成的随机数作为参数，在秒杀开始的时候才能得到</code></p></blockquote><h3 id="如何控制秒杀商品页面购买按钮的点亮" tabindex="-1">如何控制秒杀商品页面购买按钮的点亮 <a class="header-anchor" href="#如何控制秒杀商品页面购买按钮的点亮" aria-label="Permalink to &quot;如何控制秒杀商品页面购买按钮的点亮&quot;">​</a></h3><blockquote><p>购买按钮只有在秒杀开始的时候才能点亮，在此之前是灰色的。如果该页面是动态生成的，当然可以在服务器端构造响应页面输出，控制该按钮是灰色还 是点亮，但是为了减轻服务器端负载压力，更好地利用CDN、反向代理等性能优化手段，该页面被设计为静态页面，缓存在CDN、反向代理服务器上，甚至用户浏览器上。秒杀开始时，用户刷新页面，请求根本不会到达应用服务器。</p></blockquote><blockquote><p><strong>解决方案</strong>：使用JavaScript脚本控制，<code>在秒杀商品静态页面中加入一个JavaScript文件引用</code>，该JavaScript文件中包含 秒杀开始标志为否；当秒杀开始的时候生成一个新的JavaScript文件（文件名保持不变，只是内容不一样），更新秒杀开始标志为是，<code>加入下单页面的URL及随机数参数（这个随机数只会产生一个，即所有人看到的URL都是同一个，服务器端可以用redis这种分布式缓存服务器来保存随机数）</code>，并被用户浏览器加载，控制秒杀商品页面的展示。<code>这个JavaScript文件的加载可以加上随机版本号（例如xx.js?v=32353823），这样就不会被浏览器、CDN和反向代理服务器缓存</code>。</p></blockquote><blockquote><p>这个JavaScript文件非常小，即使每次浏览器刷新都访问JavaScript文件服务器也不会对服务器集群和网络带宽造成太大压力。</p></blockquote><h3 id="如何只允许第一个提交的订单被发送到订单子系统" tabindex="-1">如何只允许第一个提交的订单被发送到订单子系统 <a class="header-anchor" href="#如何只允许第一个提交的订单被发送到订单子系统" aria-label="Permalink to &quot;如何只允许第一个提交的订单被发送到订单子系统&quot;">​</a></h3><blockquote><p>由于最终能够成功秒杀到商品的用户只有一个，因此需要在用户提交订单时，检查是否已经有订单提交。如果已经有订单提交成功，则需要更新 JavaScript文件，更新秒杀开始标志为否，购买按钮变灰。事实上，由于最终能够成功提交订单的用户只有一个，为了减轻下单页面服务器的负载压力， 可以控制进入下单页面的入口，只有少数用户能进入下单页面，其他用户直接进入秒杀结束页面。</p></blockquote><blockquote><p><strong>解决方案</strong>：假设下单服务器集群有10台服务器，每台服务器只接受最多10个下单请求。在还没有人提交订单成功之前，如果一台服务器已经有十单了，而有的一单都没处理，可能出现的用户体验不佳的场景是用户第一次点击购买按钮进入已结束页面，再刷新一下页面，有可能被一单都没有处理的服务器处理，进入了填写订单的页面，<code>可以考虑通过cookie的方式来应对，符合一致性原则</code>。当然可以<code>采用最少连接的负载均衡算法</code>，出现上述情况的概率大大降低。</p></blockquote><h3 id="如何进行下单前置检查" tabindex="-1">如何进行下单前置检查 <a class="header-anchor" href="#如何进行下单前置检查" aria-label="Permalink to &quot;如何进行下单前置检查&quot;">​</a></h3><blockquote><p>**下单服务器检查本机已处理的下单请求数目：**如果超过10条，直接返回已结束页面给用户；如果未超过10条，则用户可进入填写订单及确认页面；</p></blockquote><blockquote><p>**检查全局已提交订单数目：**已超过秒杀商品总数，返回已结束页面给用户；未超过秒杀商品总数，提交到子订单系统；</p></blockquote><h3 id="秒杀一般是定时上架" tabindex="-1">秒杀一般是定时上架 <a class="header-anchor" href="#秒杀一般是定时上架" aria-label="Permalink to &quot;秒杀一般是定时上架&quot;">​</a></h3><p>该功能实现方式很多。不过目前比较好的方式是：提前设定好商品的上架时间，用户可以在前台看到该商品，但是无法点击“立即购买”的按钮。但是需要考虑的是，<code>有人可以绕过前端的限制，直接通过URL的方式发起购买</code>，这就需要在前台商品页面，以及bug页面到后端的数据库，都要进行时钟同步。越在后端控制，安全性越高。</p><p>定时秒杀的话，就要避免卖家在秒杀前对商品做编辑带来的不可预期的影响。这种特殊的变更需要多方面评估。一般禁止编辑，如需变更，可以走数据订正多的流程。</p><h3 id="减库存的操作" tabindex="-1">减库存的操作 <a class="header-anchor" href="#减库存的操作" aria-label="Permalink to &quot;减库存的操作&quot;">​</a></h3><p>有两种选择，一种是<code>拍下减库存</code> 另外一种是<code>付款减库存</code>；目前采用的<code>“拍下减库存”</code>的方式，拍下就是一瞬间的事，对用户体验会好些。</p><h3 id="库存会带来-超卖-的问题-售出数量多于库存数量" tabindex="-1">库存会带来“超卖”的问题：售出数量多于库存数量 <a class="header-anchor" href="#库存会带来-超卖-的问题-售出数量多于库存数量" aria-label="Permalink to &quot;库存会带来“超卖”的问题：售出数量多于库存数量&quot;">​</a></h3><p>由于库存并发更新的问题，导致在实际库存已经不足的情况下，库存依然在减，导致卖家的商品卖得件数超过秒杀的预期。方案：<code>采用乐观锁</code></p><div class="language-"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">update auction_auctions set</span></span>
<span class="line"><span style="color:#A6ACCD;">quantity = #inQuantity#</span></span>
<span class="line"><span style="color:#A6ACCD;">where auction_id = #itemId# and quantity = #dbQuantity#</span></span></code></pre></div><p><strong>还有一种方式，会更好些，叫做尝试扣减库存，扣减库存成功才会进行下单逻辑：</strong></p><div class="language-"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">update auction_auctions set </span></span>
<span class="line"><span style="color:#A6ACCD;">quantity = quantity-#count# </span></span>
<span class="line"><span style="color:#A6ACCD;">where auction_id = #itemId# and quantity &gt;= #count#</span></span></code></pre></div><h3 id="秒杀器的应对" tabindex="-1">秒杀器的应对 <a class="header-anchor" href="#秒杀器的应对" aria-label="Permalink to &quot;秒杀器的应对&quot;">​</a></h3><p>秒杀器一般下单个购买及其迅速，根据购买记录可以甄别出一部分。可以通过校验码达到一定的方法，这就要求校验码足够安全，不被破解，采用的方式有：<code>秒杀专用验证码，电视公布验证码，秒杀答题</code>。</p><h2 id="秒杀架构原则" tabindex="-1">秒杀架构原则 <a class="header-anchor" href="#秒杀架构原则" aria-label="Permalink to &quot;秒杀架构原则&quot;">​</a></h2><h3 id="尽量将请求拦截在系统上游" tabindex="-1">尽量将请求拦截在系统上游 <a class="header-anchor" href="#尽量将请求拦截在系统上游" aria-label="Permalink to &quot;尽量将请求拦截在系统上游&quot;">​</a></h3><blockquote><p>传统秒杀系统之所以挂，请求都压倒了后端数据层，数据读写锁冲突严重，并发高响应慢，几乎所有请求都超时，流量虽大，下单成功的有效流量甚小【一趟火车其实只有2000张票，200w个人来买，基本没有人能买成功，请求有效率为0】。</p></blockquote><h3 id="读多写少的常用多使用缓存" tabindex="-1">读多写少的常用多使用缓存 <a class="header-anchor" href="#读多写少的常用多使用缓存" aria-label="Permalink to &quot;读多写少的常用多使用缓存&quot;">​</a></h3><blockquote><p>这是一个典型的**<code>读多写少</code><strong>的应用场景【一趟火车其实只有2000张票，200w个人来买，最多2000个人下单成功，其他人都是查询库存，写比例只有0.1%，读比例占99.9%】，</strong><code>非常适合使用缓存</code>**。</p></blockquote><h2 id="秒杀架构设计" tabindex="-1">秒杀架构设计 <a class="header-anchor" href="#秒杀架构设计" aria-label="Permalink to &quot;秒杀架构设计&quot;">​</a></h2><blockquote><p>秒杀系统为秒杀而设计，不同于一般的网购行为，参与秒杀活动的用户更关心的是如何能快速刷新商品页面，在秒杀开始的时候抢先进入下单页面，而不是商品详情等用户体验细节，因此秒杀系统的页面设计应尽可能简单。商品页面中的购买按钮只有在秒杀活动开始的时候才变亮，在此之前及秒杀商品卖出后，该按钮都是灰色的，不可以点击。</p></blockquote><blockquote><p>下单表单也尽可能简单，购买数量只能是一个且不可以修改，送货地址和付款方式都使用用户默认设置，没有默认也可以不填，允许等订单提交后修改；只有第一个提交的订单发送给网站的订单子系统，其余用户提交订单后只能看到秒杀结束页面。</p></blockquote><blockquote><p>要做一个这样的秒杀系统，业务会分为两个阶段，<strong><code>第一个阶段是秒杀开始前某个时间到秒杀开始</code></strong>， 这个阶段可以称之为**<code>准备阶段</code><strong>，用户在准备阶段等待秒杀；</strong><code>第二个阶段就是秒杀开始到所有参与秒杀的用户获得秒杀结果</code><strong>， 这个就称为</strong><code>秒杀阶段</code>**吧。</p></blockquote><h3 id="前端层设计" tabindex="-1">前端层设计 <a class="header-anchor" href="#前端层设计" aria-label="Permalink to &quot;前端层设计&quot;">​</a></h3><p>首先要有一个展示秒杀商品的页面， 在这个页面上做一个秒杀活动开始的倒计时， <code>在准备阶段内用户会陆续打开这个秒杀的页面， 并且可能不停的刷新页面</code>。这里需要考虑两个问题：</p><p><strong>第一个是秒杀页面的展示</strong></p><p>我们知道一个html页面还是比较大的，<code>即使做了压缩，http头和内容的大小也可能高达数十K，加上其他的css， js，图片等资源</code>，如果同时有几千万人参与一个商品的抢购，一般机房带宽也就只有1G~10G，<code>网络带宽就极有可能成为瓶颈</code>，所以这个页面上<code>各类静态资源首先应分开存放，然后放到cdn节点上分散压力</code>，由于CDN节点遍布全国各地，能缓冲掉绝大部分的压力，而且还比机房带宽便宜~</p><p><strong>第二个是倒计时</strong></p><p>出于性能原因这个一般由js调用客户端本地时间，就有可能出现客户端时钟与服务器时钟不一致，另外服务器之间也是有可能出现时钟不一致。<code>客户端与服务器时钟不一致可以采用客户端定时和服务器同步时间</code>，这里考虑一下性能问题，<code>用于同步时间的接口由于不涉及到后端逻辑，只需要将当前web服务器的时间发送给客户端就可以了，因此速度很快</code>，就我以前测试的结果来看，一台标准的web服务器2W+QPS不会有问题，如果100W人同时刷，100W QPS也只需要50台web，一台硬件LB就可以了~，并且web服务器群是可以很容易的横向扩展的(LB+DNS轮询)，这个接口可以只返回一小段json格式的数据，而且可以优化一下减少不必要cookie和其他http头的信息，所以数据量不会很大，<code>一般来说网络不会成为瓶颈，即使成为瓶颈也可以考虑多机房专线连通，加智能DNS的解决方案</code>；web服务器之间时间不同步可以采用统一时间服务器的方式，<code>比如每隔1分钟所有参与秒杀活动的web服务器就与时间服务器做一次时间同步</code>。</p><p><strong>浏览器层请求拦截</strong></p><p>（1）<strong>产品层面</strong>，用户点击“查询”或者“购票”后，按钮置灰，禁止用户重复提交请求;</p><p>（2）<strong>JS层面</strong>，限制用户在x秒之内只能提交一次请求;</p><h3 id="站点层设计" tabindex="-1">站点层设计 <a class="header-anchor" href="#站点层设计" aria-label="Permalink to &quot;站点层设计&quot;">​</a></h3><p>前端层的请求拦截，只能拦住小白用户（不过这是99%的用户哟），高端的程序员根本不吃这一套，写个for循环，直接调用你后端的http请求，怎么整？</p><p>（1）<strong><code>同一个uid，限制访问频度</code></strong>，做页面缓存，x秒内到达站点层的请求，均返回同一页面</p><p>（2）<strong><code>同一个item的查询，例如手机车次</code></strong>，做页面缓存，x秒内到达站点层的请求，均返回同一页面</p><p>如此限流，又有99%的流量会被拦截在站点层。##4.3 服务层设计## 站点层的请求拦截，只能拦住普通程序员，高级黑客，假设他控制了10w台肉鸡（并且假设买票不需要实名认证），这下uid的限制不行了吧？怎么整？</p><p>（1）大哥，我是服务层，我清楚的知道小米只有1万部手机，我清楚的知道一列火车只有2000张车票，我透10w个请求去数据库有什么意义呢？<strong><code>对于写请求，做请求队列，每次只透过有限的写请求去数据层，如果均成功再放下一批，如果库存不够则队列里的写请求全部返回“已售完”</code></strong>；</p><p>（2）<strong><code>对于读请求，还用说么？cache来抗</code></strong>，不管是memcached还是redis，单机抗个每秒10w应该都是没什么问题的；</p><p>如此限流，只有非常少的写请求，和非常少的读缓存mis的请求会透到数据层去，又有99.9%的请求被拦住了。</p><ol><li><strong>用户请求分发模块</strong>：使用Nginx或Apache将用户的请求分发到不同的机器上。</li><li><strong>用户请求预处理模块</strong>：判断商品是不是还有剩余来决定是不是要处理该请求。</li><li><strong>用户请求处理模块</strong>：把通过预处理的请求封装成事务提交给数据库，并返回是否成功。</li><li><strong>数据库接口模块</strong>：该模块是数据库的唯一接口，负责与数据库交互，提供RPC接口供查询是否秒杀结束、剩余数量等信息。</li></ol><p><strong>用户请求预处理模块</strong></p><p>经过HTTP服务器的分发后，单个服务器的负载相对低了一些，但总量依然可能很大，如果后台商品已经被秒杀完毕，那么直接给后来的请求返回秒杀失败即可，不必再进一步发送事务了，示例代码可以如下所示：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#F78C6C;">package</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">seckill</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">org</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">apache</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">http</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">HttpRequest</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">/**</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;"> * 预处理阶段，把不必要的请求直接驳回，必要的请求添加到队列中进入下一阶段.</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;"> */</span></span>
<span class="line"><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">class</span><span style="color:#A6ACCD;"> </span><span style="color:#FFCB6B;">PreProcessor</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#89DDFF;">    </span><span style="color:#676E95;font-style:italic;">// 商品是否还有剩余</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">private</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">static</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">boolean</span><span style="color:#A6ACCD;"> reminds </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">true;</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">private</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">static</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">void</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">forbidden</span><span style="color:#89DDFF;">()</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#89DDFF;">        </span><span style="color:#676E95;font-style:italic;">// Do something.</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">static</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">boolean</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">checkReminds</span><span style="color:#89DDFF;">()</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">reminds</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#89DDFF;">            </span><span style="color:#676E95;font-style:italic;">// 远程检测是否还有剩余，该RPC接口应由数据库服务器提供，不必完全严格检查.</span></span>
<span class="line"><span style="color:#A6ACCD;">            </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(!</span><span style="color:#A6ACCD;">RPC</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">checkReminds</span><span style="color:#89DDFF;">())</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">                reminds </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">false;</span></span>
<span class="line"><span style="color:#A6ACCD;">            </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> reminds</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">    /**</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">     * 每一个HTTP请求都要经过该预处理.</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">     */</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">static</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">void</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">preProcess</span><span style="color:#89DDFF;">(</span><span style="color:#C792EA;">HttpRequest</span><span style="color:#A6ACCD;"> </span><span style="color:#A6ACCD;font-style:italic;">request</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#82AAFF;">checkReminds</span><span style="color:#89DDFF;">())</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#89DDFF;">            </span><span style="color:#676E95;font-style:italic;">// 一个并发的队列</span></span>
<span class="line"><span style="color:#A6ACCD;">            RequestQueue</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">queue</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">add</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">request</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;">}</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">else</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#89DDFF;">            </span><span style="color:#676E95;font-style:italic;">// 如果已经没有商品了，则直接驳回请求即可.</span></span>
<span class="line"><span style="color:#A6ACCD;">            </span><span style="color:#82AAFF;">forbidden</span><span style="color:#89DDFF;">();</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><p><strong>并发队列的选择</strong></p><p>Java的并发包提供了三个常用的并发队列实现，分别是：ConcurrentLinkedQueue 、 LinkedBlockingQueue 和 ArrayBlockingQueue。</p><p>ArrayBlockingQueue是<code>初始容量固定的阻塞队列</code>，我们可以用来作为数据库模块成功竞拍的队列，比如有10个商品，那么我们就设定一个10大小的数组队列。</p><p>ConcurrentLinkedQueue使用的是<code>CAS原语无锁队列实现，是一个异步队列</code>，入队的速度很快，出队进行了加锁，性能稍慢。</p><p>LinkedBlockingQueue也是<code>阻塞的队列，入队和出队都用了加锁</code>，当队空的时候线程会暂时阻塞。</p><p>由于我们的系统<code>入队需求要远大于出队需求</code>，一般不会出现队空的情况，所以我们可以选择ConcurrentLinkedQueue来作为我们的请求队列实现：</p><div class="language-"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">package seckill;</span></span>
<span class="line"><span style="color:#A6ACCD;">import java.util.concurrent.ArrayBlockingQueue;</span></span>
<span class="line"><span style="color:#A6ACCD;">import java.util.concurrent.ConcurrentLinkedQueue;</span></span>
<span class="line"><span style="color:#A6ACCD;">import org.apache.http.HttpRequest;</span></span>
<span class="line"><span style="color:#A6ACCD;">public class RequestQueue {</span></span>
<span class="line"><span style="color:#A6ACCD;">    public static ConcurrentLinkedQueue&lt;HttpRequest&gt; queue = new ConcurrentLinkedQueue&lt;HttpRequest&gt;();</span></span>
<span class="line"><span style="color:#A6ACCD;">}</span></span></code></pre></div><ul><li><strong>用户请求模块</strong></li></ul><div class="language-"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">package seckill;</span></span>
<span class="line"><span style="color:#A6ACCD;">import org.apache.http.HttpRequest;</span></span>
<span class="line"><span style="color:#A6ACCD;">public class Processor {</span></span>
<span class="line"><span style="color:#A6ACCD;">    /**</span></span>
<span class="line"><span style="color:#A6ACCD;">     * 发送秒杀事务到数据库队列.</span></span>
<span class="line"><span style="color:#A6ACCD;">     */</span></span>
<span class="line"><span style="color:#A6ACCD;">    public static void kill(BidInfo info) {</span></span>
<span class="line"><span style="color:#A6ACCD;">        DB.bids.add(info);</span></span>
<span class="line"><span style="color:#A6ACCD;">    }</span></span>
<span class="line"><span style="color:#A6ACCD;">    public static void process() {</span></span>
<span class="line"><span style="color:#A6ACCD;">        BidInfo info = new BidInfo(RequestQueue.queue.poll());</span></span>
<span class="line"><span style="color:#A6ACCD;">        if (info != null) {</span></span>
<span class="line"><span style="color:#A6ACCD;">            kill(info);</span></span>
<span class="line"><span style="color:#A6ACCD;">        }</span></span>
<span class="line"><span style="color:#A6ACCD;">    }</span></span>
<span class="line"><span style="color:#A6ACCD;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">class BidInfo {</span></span>
<span class="line"><span style="color:#A6ACCD;">    BidInfo(HttpRequest request) {</span></span>
<span class="line"><span style="color:#A6ACCD;">        // Do something.</span></span>
<span class="line"><span style="color:#A6ACCD;">    }</span></span>
<span class="line"><span style="color:#A6ACCD;">}</span></span></code></pre></div><ul><li><strong>数据库模块</strong></li></ul><p>数据库主要是使用一个ArrayBlockingQueue来暂存有可能成功的用户请求。</p><div class="language-"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">package seckill;</span></span>
<span class="line"><span style="color:#A6ACCD;">import java.util.concurrent.ArrayBlockingQueue;</span></span>
<span class="line"><span style="color:#A6ACCD;">/**</span></span>
<span class="line"><span style="color:#A6ACCD;"> * DB应该是数据库的唯一接口.</span></span>
<span class="line"><span style="color:#A6ACCD;"> */</span></span>
<span class="line"><span style="color:#A6ACCD;">public class DB {</span></span>
<span class="line"><span style="color:#A6ACCD;">    public static int count = 10;</span></span>
<span class="line"><span style="color:#A6ACCD;">    public static ArrayBlockingQueue&lt;BidInfo&gt; bids = new ArrayBlockingQueue&lt;BidInfo&gt;(10);</span></span>
<span class="line"><span style="color:#A6ACCD;">    public static boolean checkReminds() {</span></span>
<span class="line"><span style="color:#A6ACCD;">        // TODO</span></span>
<span class="line"><span style="color:#A6ACCD;">        return true;</span></span>
<span class="line"><span style="color:#A6ACCD;">    }</span></span>
<span class="line"><span style="color:#A6ACCD;">    // 单线程操作</span></span>
<span class="line"><span style="color:#A6ACCD;">    public static void bid() {</span></span>
<span class="line"><span style="color:#A6ACCD;">        BidInfo info = bids.poll();</span></span>
<span class="line"><span style="color:#A6ACCD;">        while (count-- &gt; 0) {</span></span>
<span class="line"><span style="color:#A6ACCD;">            // insert into table Bids values(item_id, user_id, bid_date, other)</span></span>
<span class="line"><span style="color:#A6ACCD;">            // select count(id) from Bids where item_id = ?</span></span>
<span class="line"><span style="color:#A6ACCD;">            // 如果数据库商品数量大约总数，则标志秒杀已完成，设置标志位reminds = false.</span></span>
<span class="line"><span style="color:#A6ACCD;">            info = bids.poll();</span></span>
<span class="line"><span style="color:#A6ACCD;">        }</span></span>
<span class="line"><span style="color:#A6ACCD;">    }</span></span>
<span class="line"><span style="color:#A6ACCD;">}</span></span></code></pre></div><h3 id="数据库设计" tabindex="-1">数据库设计 <a class="header-anchor" href="#数据库设计" aria-label="Permalink to &quot;数据库设计&quot;">​</a></h3><h4 id="基本概念" tabindex="-1">基本概念 <a class="header-anchor" href="#基本概念" aria-label="Permalink to &quot;基本概念&quot;">​</a></h4><p><strong>概念一“单库”</strong></p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFVrPNsq4MHcNGZUo2zLpiaeqQlV7XH28JTTqmNMDlj0fw8HaTB5VjlibIA/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p><strong>概念二“分片”</strong></p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFVnZRdvFflqeJQrCXrzeibLgGpZsDUXkgrSVd9bice3tkqOtHfu8Z4dtLw/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p><code>分片解决的是“数据量太大”的问题，也就是通常说的“水平切分”</code>。一旦引入分片，势必有“数据路由”的概念，哪个数据访问哪个库。路由规则通常有3种方法：</p><ol><li><strong>范围：range</strong></li></ol><p>优点：简单，容易扩展</p><p>缺点：各库压力不均（新号段更活跃）</p><ol><li><strong>哈希：hash 【大部分互联网公司采用的方案二：哈希分库，哈希路由】</strong></li></ol><p>优点：简单，数据均衡，负载均匀</p><p>缺点：迁移麻烦（2库扩3库数据要迁移）</p><ol><li><strong>路由服务：router-config-server</strong></li></ol><p>优点：灵活性强，业务与路由算法解耦</p><p>缺点：每次访问数据库前多一次查询</p><p><strong>概念三“分组”</strong></p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFVIYBP7dfeyGBvVN7E0eEgArCQMlM1VUXUEL9PZecJ9iacA6yzkECOAgA/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p>分组解决“可用性”问题，分组通常通过主从复制的方式实现。</p><p><strong>互联网公司数据库实际软件架构是：又分片，又分组（如下图）</strong></p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFVUh6uqG15udicJ02g1cGFscjbwUgicpkSIh99QraSAQnfGvHx1ncj1eCA/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><h4 id="设计思路" tabindex="-1">设计思路 <a class="header-anchor" href="#设计思路" aria-label="Permalink to &quot;设计思路&quot;">​</a></h4><p>数据库软件架构师平时设计些什么东西呢？至少要考虑以下四点：</p><ol><li>如何保证数据可用性；</li><li>如何提高数据库读性能（大部分应用读多写少，读会先成为瓶颈）；</li><li>如何保证一致性；</li><li>如何提高扩展性；</li></ol><p><strong>如何保证数据的可用性？</strong></p><div class="language-"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">解决可用性问题的思路是=&gt;冗余</span></span></code></pre></div><p>如何保证站点的可用性？复制站点，冗余站点</p><p>如何保证服务的可用性？复制服务，冗余服务</p><p>如何保证数据的可用性？复制数据，冗余数据</p><p><code>数据的冗余，会带来一个副作用=&gt;引发一致性问题（先不说一致性问题，先说可用性）</code>。</p><p><strong>如何保证数据库“读”高可用？</strong></p><div class="language-"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">冗余读库</span></span></code></pre></div><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFVfzSQxiaMKG7PEibNQY20qicrtH9g3z1hqYKQSRxiaHYWkLQ0TicAhmgrz5g/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><div class="language-"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">冗余读库带来的副作用？读写有延时，可能不一致</span></span></code></pre></div><p>上面这个图是很多互联网公司mysql的架构，写仍然是单点，不能保证写高可用。</p><p><strong>如何保证数据库“写”高可用？</strong></p><div class="language-"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">冗余写库</span></span></code></pre></div><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFV93Dj4qNQR8o6fMLtV2KVRRIuzFM4QwRC7waRxwudoKvpvBwRic5NTog/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p><code>采用双主互备的方式，可以冗余写库带来的副作用？双写同步，数据可能冲突（例如“自增id”同步冲突）</code>,如何解决同步冲突，有两种常见解决方案：</p><ol><li>两个写库使用不同的初始值，相同的步长来增加id：1写库的id为0,2,4,6...；2写库的id为1,3,5,7...；</li><li>不使用数据的id，业务层自己生成唯一的id，保证数据不冲突；</li></ol><p>实际中没有使用上述两种架构来做读写的“高可用”，<code>采用的是“双主当主从用”的方式</code>：</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFVdTkhx4dBbo9SHba52cU7HFejgDqSpWXd2Ix98oianGtmQSUflgukmLw/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p>仍是双主，但<code>只有一个主提供服务（读+写），另一个主是“shadow-master”，只用来保证高可用，平时不提供服务</code>。master挂了，shadow-master顶上（vip漂移，对业务层透明，不需要人工介入）。这种方式的好处：</p><ol><li>读写没有延时；</li><li>读写高可用；</li></ol><p>不足：</p><ol><li>不能通过加从库的方式扩展读性能；</li><li>资源利用率为50%，一台冗余主没有提供服务；</li></ol><p>那如何提高读性能呢？进入第二个话题，如何提供读性能。</p><ul><li><strong>4. 如何扩展读性能</strong></li></ul><p>提高读性能的方式大致有三种，<code>第一种是建立索引</code>。这种方式不展开，要提到的一点是，<code>不同的库可以建立不同的索引</code>。</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFVcynRibJlbXccUL7DIHM0Dd9D5ciaia0GTIiasiapLg2TZ5JdAWB8RibH6crQ/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p><code>写库</code>不建立索引；</p><p><code>线上读库</code>建立线上访问索引，例如uid；</p><p><code>线下读库</code>建立线下访问索引，例如time；</p><p><code>第二种扩充读性能的方式是，增加从库</code>，这种方法大家用的比较多，但是，存在两个缺点：</p><ol><li>从库越多，同步越慢；</li><li>同步越慢，数据不一致窗口越大（不一致后面说，还是先说读性能的提高）；</li></ol><p>实际中没有采用这种方法提高数据库读性能（没有从库），<code>采用的是增加缓存</code>。常见的缓存架构如下：</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFV3frqIVvLwo6wzGUPPjspD4ZLqO70TcNwD7ImVHcRg8Gq2C1LOzA34w/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p><code>上游是业务应用，下游是主库，从库（读写分离），缓存</code>。</p><p>实际的玩法：<code>服务+数据库+缓存一套</code></p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFVZSD2lPluyBDMYlNyvKNC5PhDEBxQbvAhaPndLBp1dBrREIwaX3dPpA/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p>业务层不直接面向db和cache，<code>服务层屏蔽了底层db、cache的复杂性</code>。为什么要引入服务层，今天不展开，采用了“服务+数据库+缓存一套”的方式提供数据访问，<code>用cache提高读性能</code>。</p><p>不管采用主从的方式扩展读性能，还是缓存的方式扩展读性能，数据都要复制多份（主+从，db+cache），<code>一定会引发一致性问题</code>。</p><ul><li><strong>5. 如何保证一致性？</strong></li></ul><p>主从数据库的一致性，通常有两种解决方案：</p><p><strong>1. 中间件</strong></p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFV4WSg0ib6eezMM9cplst7LxJtZKIPTwdWibBDApm9wrZJEqso1b0tE2bg/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p>如果某一个key有写操作，在不一致时间窗口内，中间件会将这个key的读操作也路由到主库上。这个方案的缺点是，<code>数据库中间件的门槛较高</code>。</p><p><strong>2. 强制读主</strong></p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFVSTYTW5gNYnPq7YicL3YWuRdtKyy6zF2oYLsxSnUl0dMoFZOB2yJXWTw/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p><code>上面实际用的“双主当主从用”的架构，不存在主从不一致的问题</code>。</p><p>第二类不一致，<code>是db与缓存间的不一致</code>：</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFV3frqIVvLwo6wzGUPPjspD4ZLqO70TcNwD7ImVHcRg8Gq2C1LOzA34w/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p>常见的缓存架构如上，此时<strong>写操作</strong>的顺序是：</p><p>（1）淘汰cache；</p><p>（2）写数据库；</p><p><strong>读操作</strong>的顺序是：</p><p>（1）读cache，如果cache hit则返回；</p><p>（2）如果cache miss，则读从库；</p><p>（3）读从库后，将数据放回cache；</p><p>在一些异常时序情况下，有可能从【从库读到旧数据（同步还没有完成），旧数据入cache后】，数据会长期不一致。<code>解决办法是“缓存双淘汰”</code>，写操作时序升级为：</p><p>（1）淘汰cache；</p><p>（2）写数据库；</p><p>（3）在经验“主从同步延时窗口时间”后，再次发起一个异步淘汰cache的请求；</p><p>这样，即使有脏数据如cache，一个小的时间窗口之后，脏数据还是会被淘汰。带来的代价是，多引入一次读miss（成本可以忽略）。</p><p>除此之外，最佳实践之一是：<code>建议为所有cache中的item设置一个超时时间</code>。</p><ul><li><strong>6. 如何提高数据库的扩展性？</strong></li></ul><p>原来用hash的方式路由，分为2个库，数据量还是太大，要分为3个库，势必需要进行数据迁移，有一个很帅气的“数据库秒级扩容”方案。</p><p><strong>如何秒级扩容？</strong></p><p>首先，<code>我们不做2库变3库的扩容，我们做2库变4库（库加倍）的扩容（未来4-&gt;8-&gt;16）</code></p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFVScCiaXEIcXcGs1pTgq39mBATe8qJkOShqXWXZjibAibLc6C3xxBEqxbicg/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p>服务+数据库是一套（省去了缓存），<code>数据库采用“双主”的模式</code>。</p><p><strong>扩容步骤：</strong></p><p><code>第一步</code>，将一个主库提升;</p><p><code>第二步</code>，修改配置，2库变4库（原来MOD2，现在配置修改后MOD4），扩容完成；</p><p><code>原MOD2为偶的部分，现在会MOD4余0或者2；原MOD2为奇的部分，现在会MOD4余1或者3</code>；数据不需要迁移，同时，双主互相同步，一遍是余0，一边余2，两边数据同步也不会冲突，秒级完成扩容！</p><p>最后，要做一些收尾工作：</p><ol><li>将旧的双主同步解除；</li><li>增加新的双主（双主是保证可用性的，shadow-master平时不提供服务）；</li><li>删除多余的数据（余0的主，可以将余2的数据删除掉）；</li></ol><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFVmSUFY8libTxpwd2Mv1JQOPNOGpiaj7UwflLgsDsEn0TY7VClk6H4G8Kg/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p>这样，秒级别内，我们就完成了2库变4库的扩展。</p><h2 id="大并发带来的挑战" tabindex="-1">大并发带来的挑战 <a class="header-anchor" href="#大并发带来的挑战" aria-label="Permalink to &quot;大并发带来的挑战&quot;">​</a></h2><h3 id="请求接口的合理设计" tabindex="-1">请求接口的合理设计 <a class="header-anchor" href="#请求接口的合理设计" aria-label="Permalink to &quot;请求接口的合理设计&quot;">​</a></h3><p>一个秒杀或者抢购页面，通常分为2个部分，一个是<code>静态的HTML等内容</code>，另一个就是<code>参与秒杀的Web后台请求接口</code>。</p><p><code>通常静态HTML等内容，是通过CDN的部署，一般压力不大，核心瓶颈实际上在后台请求接口上</code>。这个后端接口，必须能够支持高并发请求，同时，非常重要的一点，必须尽可能“快”，在最短的时间里返回用户的请求结果。<code>为了实现尽可能快这一点，接口的后端存储使用内存级别的操作会更好一点</code>。仍然直接面向MySQL之类的存储是不合适的，<code>如果有这种复杂业务的需求，都建议采用异步写入</code>。</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFVQ5hQsI4qwKYiaoxJkQZRdibCSCaxnt25G8pdMfdaXko2YibIGtDZ0EaRA/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p>当然，也有一些秒杀和抢购<code>采用“滞后反馈”</code>，就是说秒杀当下不知道结果，一段时间后才可以从页面中看到用户是否秒杀成功。但是，这种属于“偷懒”行为，同时给用户的体验也不好，容易被用户认为是“暗箱操作”。</p><h3 id="高并发的挑战-一定要-快" tabindex="-1">高并发的挑战：一定要“快” <a class="header-anchor" href="#高并发的挑战-一定要-快" aria-label="Permalink to &quot;高并发的挑战：一定要“快”&quot;">​</a></h3><p>我们通常衡量一个<code>Web系统的吞吐率的指标是QPS（Query Per Second，每秒处理请求数），解决每秒数万次的高并发场景，这个指标非常关键</code>。举个例子，我们假设处理一个业务请求平均响应时间为100ms，同时，系统内有20台Apache的Web服务器，配置MaxClients为500个（表示Apache的最大连接数目）。</p><p>那么，我们的Web系统的理论峰值QPS为（理想化的计算方式）：</p><div class="language-"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">20*500/0.1 = 100000 （10万QPS）</span></span></code></pre></div><p>咦？我们的系统似乎很强大，1秒钟可以处理完10万的请求，5w/s的秒杀似乎是“纸老虎”哈。实际情况，当然没有这么理想。<code>在高并发的实际场景下，机器都处于高负载的状态，在这个时候平均响应时间会被大大增加</code>。</p><p><code>就Web服务器而言，Apache打开了越多的连接进程，CPU需要处理的上下文切换也越多，额外增加了CPU的消耗，然后就直接导致平均响应时间增加</code>。因此上述的<code>MaxClient数目，要根据CPU、内存等硬件因素综合考虑，绝对不是越多越好</code>。可以<code>通过Apache自带的abench来测试一下</code>，取一个合适的值。然后，我们<code>选择内存操作级别的存储的Redis，在高并发的状态下，存储的响应时间至关重要</code>。网络带宽虽然也是一个因素，不过，这种请求数据包一般比较小，一般很少成为请求的瓶颈。负载均衡成为系统瓶颈的情况比较少，在这里不做讨论哈。</p><p>那么问题来了，假设我们的系统，在5w/s的高并发状态下，平均响应时间从100ms变为250ms（实际情况，甚至更多）：</p><div class="language-"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">20*500/0.25 = 40000 （4万QPS）</span></span></code></pre></div><p>于是，我们的系统剩下了4w的QPS，面对5w每秒的请求，中间相差了1w。</p><p>然后，这才是真正的恶梦开始。举个例子，高速路口，1秒钟来5部车，每秒通过5部车，高速路口运作正常。突然，这个路口1秒钟只能通过4部车，车流量仍然依旧，结果必定出现大塞车。（5条车道忽然变成4条车道的感觉）。</p><p>同理，某一个秒内，20*500个可用连接进程都在满负荷工作中，却仍然有1万个新来请求，没有连接进程可用，系统陷入到异常状态也是预期之内。</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFVtq9bqxyElVkfsX9d2ibgzGG0icV59paKHdq9h5xCg9p2NwC8OEqutLqQ/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p>其实在正常的非高并发的业务场景中，也有类似的情况出现，某个业务请求接口出现问题，响应时间极慢，将整个Web请求响应时间拉得很长，逐渐将Web服务器的可用连接数占满，其他正常的业务请求，无连接进程可用。</p><p>更可怕的问题是，是用户的行为特点，系统越是不可用，用户的点击越频繁，<code>恶性循环最终导致“雪崩”（其中一台Web机器挂了，导致流量分散到其他正常工作的机器上，再导致正常的机器也挂，然后恶性循环）</code>，将整个Web系统拖垮。</p><h3 id="重启与过载保护" tabindex="-1">重启与过载保护 <a class="header-anchor" href="#重启与过载保护" aria-label="Permalink to &quot;重启与过载保护&quot;">​</a></h3><p>如果系统发生“雪崩”，贸然重启服务，是无法解决问题的。最常见的现象是，启动起来后，立刻挂掉。这个时候，<code>最好在入口层将流量拒绝，然后再将重启</code>。<code>如果是redis/memcache这种服务也挂了，重启的时候需要注意“预热”，并且很可能需要比较长的时间</code>。</p><p>秒杀和抢购的场景，流量往往是超乎我们系统的准备和想象的。这个时候，过载保护是必要的。<code>如果检测到系统满负载状态，拒绝请求也是一种保护措施</code>。在前端设置过滤是最简单的方式，但是，这种做法是被用户“千夫所指”的行为。更合适一点的是，<code>将过载保护设置在CGI入口层，快速将客户的直接请求返回</code>。</p><h2 id="作弊的手段-进攻与防守" tabindex="-1">作弊的手段：进攻与防守 <a class="header-anchor" href="#作弊的手段-进攻与防守" aria-label="Permalink to &quot;作弊的手段：进攻与防守&quot;">​</a></h2><p>秒杀和抢购收到了“海量”的请求，实际上里面的水分是很大的。不少用户，为了“抢“到商品，会使用“刷票工具”等类型的辅助工具，帮助他们发送尽可能多的请求到服务器。还有一部分高级用户，制作强大的自动请求脚本。<code>这种做法的理由也很简单，就是在参与秒杀和抢购的请求中，自己的请求数目占比越多，成功的概率越高</code>。</p><p>这些都是属于“作弊的手段”，不过，有“进攻”就有“防守”，这是一场没有硝烟的战斗哈。</p><h3 id="同一个账号-一次性发出多个请求" tabindex="-1">同一个账号，一次性发出多个请求 <a class="header-anchor" href="#同一个账号-一次性发出多个请求" aria-label="Permalink to &quot;同一个账号，一次性发出多个请求&quot;">​</a></h3><p>部分用户通过浏览器的插件或者其他工具，在秒杀开始的时间里，<code>以自己的账号，一次发送上百甚至更多的请求</code>。实际上，这样的用户破坏了秒杀和抢购的公平性。</p><p>这种请求在某些没有做数据安全处理的系统里，也可能造成另外一种破坏，导致某些判断条件被绕过。例如一个简单的领取逻辑，先判断用户是否有参与记录，如果没有则领取成功，最后写入到参与记录中。这是个非常简单的逻辑，但是，在高并发的场景下，存在深深的漏洞。<code>多个并发请求通过负载均衡服务器，分配到内网的多台Web服务器，它们首先向存储发送查询请求，然后，在某个请求成功写入参与记录的时间差内，其他的请求获查询到的结果都是“没有参与记录”</code>。这里，就存在逻辑判断被绕过的风险。</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFVTv3w9t1NpqaN1FTJiaYxdiar2X03yugc2BWr51du4w2sv5deicJibdM8cg/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p><strong>应对方案：</strong></p><p>在程序入口处，一个账号只允许接受1个请求，其他请求过滤。不仅解决了同一个账号，发送N个请求的问题，还保证了后续的逻辑流程的安全。<code>实现方案，可以通过Redis这种内存缓存服务，写入一个标志位（只允许1个请求写成功，结合watch的乐观锁的特性），成功写入的则可以继续参加</code>。</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFVAHFlhic0zbxGlqM88xbqywLDaLHDbzLHcn3alQX7vqPHtyCFHThcHlA/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p>或者，自己实现一个服务，将同一个账号的请求放入一个队列中，处理完一个，再处理下一个。</p><h3 id="多个账号-一次性发送多个请求" tabindex="-1">多个账号，一次性发送多个请求 <a class="header-anchor" href="#多个账号-一次性发送多个请求" aria-label="Permalink to &quot;多个账号，一次性发送多个请求&quot;">​</a></h3><p>很多公司的账号注册功能，在发展早期几乎是没有限制的，很容易就可以注册很多个账号。因此，<code>也导致了出现了一些特殊的工作室，通过编写自动注册脚本，积累了一大批“僵尸账号”，数量庞大，几万甚至几十万的账号不等，专门做各种刷的行为（这就是微博中的“僵尸粉“的来源）</code>。举个例子，例如微博中有转发抽奖的活动，如果我们使用几万个“僵尸号”去混进去转发，这样就可以大大提升我们中奖的概率。</p><p>这种账号，使用在秒杀和抢购里，也是同一个道理。例如，iPhone官网的抢购，火车票黄牛党。</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFVpCzghZZCMnbh2RvVGh1lQANOegrM4jyaWOIMrV5gysftXWVHq5Y4Mg/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p><strong>应对方案：</strong></p><p>这种场景，可以<code>通过检测指定机器IP请求频率就可以解决，如果发现某个IP请求频率很高，可以给它弹出一个验证码或者直接禁止它的请求</code>：</p><ol><li><code>弹出验证码，最核心的追求，就是分辨出真实用户</code>。因此，大家可能经常发现，网站弹出的验证码，有些是“鬼神乱舞”的样子，有时让我们根本无法看清。他们这样做的原因，其实也是为了让验证码的图片不被轻易识别，因为强大的“自动脚本”可以通过图片识别里面的字符，然后让脚本自动填写验证码。实际上，有一些非常创新的验证码，效果会比较好，例如给你一个简单问题让你回答，或者让你完成某些简单操作（例如百度贴吧的验证码）。</li><li><code>直接禁止IP，实际上是有些粗暴的，因为有些真实用户的网络场景恰好是同一出口IP的，可能会有“误伤“</code>。但是这一个做法简单高效，根据实际场景使用可以获得很好的效果。</li></ol><h3 id="多个账号-不同ip发送不同请求" tabindex="-1">多个账号，不同IP发送不同请求 <a class="header-anchor" href="#多个账号-不同ip发送不同请求" aria-label="Permalink to &quot;多个账号，不同IP发送不同请求&quot;">​</a></h3><p>所谓道高一尺，魔高一丈。有进攻，就会有防守，永不休止。<code>这些“工作室”，发现你对单机IP请求频率有控制之后，他们也针对这种场景，想出了他们的“新进攻方案”，就是不断改变IP</code>。</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFVv3q75ib1f17xfmcianQ5sUdpqX6HiceaRXFO6c6nnSIFYtGVBMnncoc4g/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p>有同学会好奇，这些随机IP服务怎么来的。<code>有一些是某些机构自己占据一批独立IP，然后做成一个随机代理IP的服务，有偿提供给这些“工作室”使用</code>。还有一些更为黑暗一点的，就是<code>通过木马黑掉普通用户的电脑，这个木马也不破坏用户电脑的正常运作，只做一件事情，就是转发IP包，普通用户的电脑被变成了IP代理出口</code>。通过这种做法，黑客就拿到了大量的独立IP，然后搭建为随机IP服务，就是为了挣钱。</p><p><strong>应对方案：</strong></p><p>说实话，这种场景下的请求，和真实用户的行为，已经基本相同了，想做分辨很困难。再做进一步的限制很容易“误伤“真实用户，这个时候，<code>通常只能通过设置业务门槛高来限制这种请求了，或者通过账号行为的”数据挖掘“来提前清理掉它们</code>。</p><p>僵尸账号也还是有一些共同特征的，例如<code>账号很可能属于同一个号码段甚至是连号的，活跃度不高，等级低，资料不全等等</code>。根据这些特点，适当设置参与门槛，例如限制参与秒杀的账号等级。<code>通过这些业务手段，也是可以过滤掉一些僵尸号</code>。</p><h2 id="高并发下的数据安全" tabindex="-1">高并发下的数据安全 <a class="header-anchor" href="#高并发下的数据安全" aria-label="Permalink to &quot;高并发下的数据安全&quot;">​</a></h2><p>我们知道在<code>多线程写入同一个文件的时候，会存现“线程安全”的问题</code>（多个线程同时运行同一段代码，如果每次运行结果和单线程运行的结果是一样的，结果和预期相同，就是线程安全的）。<code>如果是MySQL数据库，可以使用它自带的锁机制很好的解决问题，但是，在大规模并发的场景中，是不推荐使用MySQL的</code>。秒杀和抢购的场景中，还有另外一个问题，就是“超发”，如果在这方面控制不慎，会产生发送过多的情况。我们也曾经听说过，某些电商搞抢购活动，买家成功拍下后，商家却不承认订单有效，拒绝发货。这里的问题，也许并不一定是商家奸诈，而是系统技术层面存在超发风险导致的。</p><h3 id="超发的原因" tabindex="-1">超发的原因 <a class="header-anchor" href="#超发的原因" aria-label="Permalink to &quot;超发的原因&quot;">​</a></h3><p>假设某个抢购场景中，我们一共只有100个商品，在最后一刻，我们已经消耗了99个商品，仅剩最后一个。这个时候，系统发来多个并发请求，这批请求读取到的商品余量都是99个，然后都通过了这一个余量判断，最终导致超发。</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFVnMfseOOk4HiaU53RJSTxY2zSpCgRSvszZEetP2iaBYXduibzXNbrZhkMg/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p>在上面的这个图中，就导致了并发用户B也“抢购成功”，多让一个人获得了商品。这种场景，在高并发的情况下非常容易出现。</p><h3 id="悲观锁思路" tabindex="-1">悲观锁思路 <a class="header-anchor" href="#悲观锁思路" aria-label="Permalink to &quot;悲观锁思路&quot;">​</a></h3><p>解决线程安全的思路很多，可以从“悲观锁”的方向开始讨论。</p><div class="language-"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">悲观锁，也就是在修改数据的时候，采用锁定状态，排斥外部请求的修改。遇到加锁的状态，就必须等待。</span></span></code></pre></div><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFVWWOkXIpZ45v2gxkoAgBExia9hHwIOibKnzwWCjFOgFqibIA0JIC5Wgzibg/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p>虽然上述的方案的确解决了线程安全的问题，但是，别忘记，<code>我们的场景是“高并发”。也就是说，会很多这样的修改请求，每个请求都需要等待“锁”，某些线程可能永远都没有机会抢到这个“锁”，这种请求就会死在那里</code>。同时，这种请求会很多，<code>瞬间增大系统的平均响应时间，结果是可用连接数被耗尽，系统陷入异常</code>。</p><h3 id="fifo队列思路" tabindex="-1">FIFO队列思路 <a class="header-anchor" href="#fifo队列思路" aria-label="Permalink to &quot;FIFO队列思路&quot;">​</a></h3><p>那好，那么我们稍微修改一下上面的场景，<code>我们直接将请求放入队列中的，采用FIFO（First Input First Output，先进先出），这样的话，我们就不会导致某些请求永远获取不到锁</code>。看到这里，是不是有点强行将多线程变成单线程的感觉哈。</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFVRzEpia2zv9C9srhSlic5KoYDH2KwPnw44xeUAlzx3OfYEc4ibvXFDUrpg/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p>然后，我们现在解决了锁的问题，全部请求采用“先进先出”的队列方式来处理。那么新的问题来了，<code>高并发的场景下，因为请求很多，很可能一瞬间将队列内存“撑爆”，然后系统又陷入到了异常状态</code>。或者设计一个极大的内存队列，也是一种方案，但是，系统处理完一个队列内请求的速度根本无法和疯狂涌入队列中的数目相比。也就是说，队列内的请求会越积累越多，最终Web系统平均响应时候还是会大幅下降，系统还是陷入异常。</p><h3 id="乐观锁思路" tabindex="-1">乐观锁思路 <a class="header-anchor" href="#乐观锁思路" aria-label="Permalink to &quot;乐观锁思路&quot;">​</a></h3><p>这个时候，我们就可以讨论一下“乐观锁”的思路了。<code>乐观锁，是相对于“悲观锁”采用更为宽松的加锁机制，大都是采用带版本号（Version）更新。实现就是，这个数据所有请求都有资格去修改，但会获得一个该数据的版本号，只有版本号符合的才能更新成功，其他的返回抢购失败</code>。这样的话，我们就不需要考虑队列的问题，不过，<code>它会增大CPU的计算开销</code>。但是，综合来说，这是一个比较好的解决方案。</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/1QxwhpDy7ia0ibHKyDrXZceLf5DgibrPyFVBs8PrJ4oMI0PF2mWeL2peYYmicm8NZVZLt1Vv4tQFo8xjRr6WvoibHVg/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p>有很多软件和服务都“乐观锁”功能的支持，例如<code>Redis中的watch就是其中之一</code>。通过这个实现，我们保证了数据的安全。</p><h1 id="秒杀架构设计必杀技" tabindex="-1">秒杀架构设计必杀技 <a class="header-anchor" href="#秒杀架构设计必杀技" aria-label="Permalink to &quot;秒杀架构设计必杀技&quot;">​</a></h1><p>今天我从 7 个不同的维度，讲讲秒杀系统的架构设计，主要知识点如下：</p><ul><li>Nginx + 前后端分离 + CDN 缓存 + 网关（限流+熔断）</li><li>集群的路由层 + Redis（缓存热点数据、分布式锁)</li><li>MQ 集群</li><li>业务处理层</li><li>数据库层（读写分离、热点隔离)</li></ul><h2 id="秒杀业务的特点" tabindex="-1">秒杀业务的特点 <a class="header-anchor" href="#秒杀业务的特点" aria-label="Permalink to &quot;秒杀业务的特点&quot;">​</a></h2><p><img src="https://mmbiz.qpic.cn/mmbiz/OKUeiaP72uRz0oGF9fvHiaCpTG5jfcD9nUppVT1ac1UZ7klxdbia7sJVXiaXAy4qhYCD7sDN8UrjxYtHzzZq2rvfBQ/640?wx_fmt=jpeg&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><ul><li>瞬间大量的刷新页面的操作</li><li>瞬间大量的抢宝的操作</li><li>可能有秒杀器的恶性竞争</li></ul><h2 id="总体思路" tabindex="-1">总体思路 <a class="header-anchor" href="#总体思路" aria-label="Permalink to &quot;总体思路&quot;">​</a></h2><h3 id="削峰限流" tabindex="-1">削峰限流 <a class="header-anchor" href="#削峰限流" aria-label="Permalink to &quot;削峰限流&quot;">​</a></h3><ul><li>前端+Redis拦截，只有redis扣减成功的请求才能进入到下游</li><li>MQ堆积订单，保护订单处理层的负载，Consumer根据自己的消费能力来取Task，实际上下游的压力就可控了。重点做好路由层和MQ的安全</li><li>引入答题验证码、请求的随机休眠等措施，削峰填谷</li></ul><p><strong>安全保护</strong></p><ul><li>页面和前端要做判断，防止活动未开始就抢单，防止重复点击按钮连续抢单</li><li>防止秒杀器恶意抢单，IP限流、UserId限流限购、引入答题干扰答题器，并且对答题器答题时间做常理推断</li><li>IP黑名单、UserId黑名单功能</li><li>过载丢弃：QPS或者CPU等核心指标超过一定限额时，丢弃请求，避免服务器挂掉，保证大部分用户可用</li></ul><p><strong>页面优化，动静分离</strong></p><ul><li>秒杀商品的网页内容尽可能做的简单：图片小、js css 体积小数量少，内容尽可能的做到动静分离</li><li>秒杀的抢宝过程中做成异步刷新抢宝，而不需要用户刷新页面来抢，降低服务器交互的压力</li><li>可以使用Nginx的动静分离，不通过传统web浏览器获取静态资源</li><li>nginx开启gzip压缩，压缩静态资源，减少传输带宽，提升传输速度</li><li>或者使用Varnish，把静态资源缓存到内存当中，避免静态资源的获取给服务器造成的压力</li></ul><p><strong>异步处理</strong></p><ul><li>redis抢单成功后，把后续的业务丢到线程池中异步的处理，提高抢单的响应速度</li><li>线程池处理时，把任务丢到MQ中，异步的等待各个子系统处理（订单系统、库存系统、支付系统、优惠券系统） 异步操作有事务问题，本地事务和分布式事务，但是为了提升并发度，最好牺牲一致性。通过定时扫描统计日志，来发现有问题的订单，并且及时处理</li></ul><p><strong>热点分离</strong></p><p>尽量的避免秒杀功能给正常功能带来的影响，比如秒杀把服务器某个功能拖垮了。</p><p>分离可以提升系统的容灾性，但是完全的隔离的改造成本太高了，尽量借助中间件的配置，来实现冷热分离。</p><ul><li><p>集群节点的分离：nginx配置让秒杀业务走的集群节点和普通业务走的集群不一样。</p></li><li><p>MQ的分离：避免秒杀业务把消息队列堆满了，普通业务的交易延迟也特别厉害。</p></li><li><p>数据库的分离：根据实际的秒杀的QPS来选择，热点数据分库以后，增加了分布式事务的问题，以及查询的时候跨库查询性能要差一些（ShardingJDBC有这种功能），所以要权衡以后再决定是否需要分库</p></li><li><p>避免单点：各个环节都要尽力避免</p></li><li><p>降级：临时关闭一些没那么重要的功能，比如秒杀商品的转赠功能、红包的提现功能，待秒杀峰值过了，设置开关，再动态开放这些次要的功能</p></li></ul><h3 id="nginx的设计细节" tabindex="-1">Nginx的设计细节 <a class="header-anchor" href="#nginx的设计细节" aria-label="Permalink to &quot;Nginx的设计细节&quot;">​</a></h3><p>动静分离，不走tomcat获取静态资源</p><div class="language-"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">server {</span></span>
<span class="line"><span style="color:#A6ACCD;">        listen       8088;</span></span>
<span class="line"><span style="color:#A6ACCD;">    location ~ \.(gif|jpg|jpeg|png|bmp|swf)$ {  </span></span>
<span class="line"><span style="color:#A6ACCD;">        root    C:/Users/502764158/Desktop/test;  </span></span>
<span class="line"><span style="color:#A6ACCD;">    } </span></span>
<span class="line"><span style="color:#A6ACCD;"></span></span>
<span class="line"><span style="color:#A6ACCD;">    location ~ \.(jsp|do)$ {</span></span>
<span class="line"><span style="color:#A6ACCD;">            proxy_pass :8082;</span></span>
<span class="line"><span style="color:#A6ACCD;">        }</span></span>
<span class="line"><span style="color:#A6ACCD;">    }</span></span>
<span class="line"><span style="color:#A6ACCD;"> }</span></span></code></pre></div><ul><li>gzip压缩，减少静态文件传输的体积，节省带宽，提高渲染速度</li></ul><div class="language-"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">gzip on;</span></span>
<span class="line"><span style="color:#A6ACCD;">    gzip_min_length 1k;</span></span>
<span class="line"><span style="color:#A6ACCD;">    gzip_buffers 4 16k;</span></span>
<span class="line"><span style="color:#A6ACCD;">    gzip_comp_level 3;</span></span>
<span class="line"><span style="color:#A6ACCD;">    gzip_disable &quot;MSIE [1-6]\.&quot;;</span></span>
<span class="line"><span style="color:#A6ACCD;">    gzip_types   text/plain application/x-javascript text/css application/xml text/javascript image/jpeg image/gif image/png;</span></span></code></pre></div><p>配置集群负载和容灾，设置失效重连的时间，失效后，定期不会再重试挂掉的节点，参数：</p><ul><li>fail_timeout默认为10s</li><li>max_fails默认为1。就是说，只要某个server失效一次，则在接下来的10s内，就不会分发请求到该server上</li><li>proxy_connect_timeout 后端服务器连接的超时时间_发起握手等候响应超时时间</li></ul><div class="language-"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">upstream  netitcast.com {  #服务器集群名字   </span></span>
<span class="line"><span style="color:#A6ACCD;">    server    127.0.0.1:8080;</span></span>
<span class="line"><span style="color:#A6ACCD;">    server    127.0.0.1:38083;</span></span>
<span class="line"><span style="color:#A6ACCD;">    server    127.0.0.1:8083;</span></span>
<span class="line"><span style="color:#A6ACCD;">    } </span></span>
<span class="line"><span style="color:#A6ACCD;"></span></span>
<span class="line"><span style="color:#A6ACCD;"> server {</span></span>
<span class="line"><span style="color:#A6ACCD;">        listen       88;</span></span>
<span class="line"><span style="color:#A6ACCD;">        server_name  localhost;</span></span>
<span class="line"><span style="color:#A6ACCD;">    location / {  </span></span>
<span class="line"><span style="color:#A6ACCD;">            proxy_pass http://netitcast.com;  </span></span>
<span class="line"><span style="color:#A6ACCD;">            proxy_connect_timeout       1;</span></span>
<span class="line"><span style="color:#A6ACCD;">            fail_timeout 5;</span></span>
<span class="line"><span style="color:#A6ACCD;">        } </span></span>
<span class="line"><span style="color:#A6ACCD;">    }</span></span></code></pre></div><ol><li>集成Varnish做静态资源的缓存</li><li>集成tengine做过载的保护</li></ol><h3 id="页面优化细节" tabindex="-1">页面优化细节 <a class="header-anchor" href="#页面优化细节" aria-label="Permalink to &quot;页面优化细节&quot;">​</a></h3><p><strong>降低交互的压力</strong></p><ul><li>尽量把js、css文件放在少数几个里面，减少浏览器和后端交互获取静态资源的次数</li><li>尽量避免在秒杀商品页面使用大的图片，或者使用过多的图片</li></ul><p><strong>安全控制</strong></p><ul><li>时间有效性验证：未到秒杀时间不能进行抢单，并且同时程序后端也要做时间有效性验证，因为网页的时间和各自的系统时间决定，而且秒杀器可以通过绕开校验直接调用抢单</li><li>异步抢单：通过点击按钮刷新抢宝，而不是刷新页面的方式抢宝（答题验证码等等也是ajax交互）</li><li>另外，搜索公众号Linux就该这样学后台回复“猴子”，获取一份惊喜礼包。</li><li>redis做IP限流</li><li>redis做UserId限流</li></ul><h3 id="redis集群的应用" tabindex="-1">Redis集群的应用 <a class="header-anchor" href="#redis集群的应用" aria-label="Permalink to &quot;Redis集群的应用&quot;">​</a></h3><ol><li>分布式锁（悲观锁）</li><li>缓存热点数据（库存）：如果QPS太高的话，另一种方案是通过localcache，分布式状态一致性通过数据库来控制</li></ol><p><strong>分布式悲观锁（参考redis悲观锁的代码）</strong></p><ul><li>悲观锁（因为肯定争抢严重）</li><li>Expire时间（抢到锁后，立刻设置过期时间，防止某个线程的异常停摆，导致整个业务的停摆）</li><li>定时循环和快速反馈（for缓存有超时设置，每次超时后，重新读取一次库存，还有货再进行第二轮的for循环争夺，实现快速反馈，避免没有货了还在持续抢锁）</li></ul><p><strong>异步处理订单</strong></p><ul><li>redis抢锁成功后，记录抢到锁的用户信息后，就可以直接释放锁，并反馈用户，通过异步的方式来处理订单，提升秒杀的效率，降低无意义的线程等待</li><li>为了避免异步的数据不同步，需要抢到锁的时候，在redis里面缓存用户信息列表，缓存结束后，触发抢单成功用户信息持久化，并且定时的比对一致性</li></ul><h3 id="消息队列限流" tabindex="-1">消息队列限流 <a class="header-anchor" href="#消息队列限流" aria-label="Permalink to &quot;消息队列限流&quot;">​</a></h3><p>消息队列削峰限流(RocketMQ自带的Consumer自带线程池和限流措施)，集群。一般都是微服务，订单中心、库存中心、积分中心、用户的商品中心</p><h3 id="数据库设计-1" tabindex="-1">数据库设计 <a class="header-anchor" href="#数据库设计-1" aria-label="Permalink to &quot;数据库设计&quot;">​</a></h3><ul><li>拆分事务提高并发度</li><li>根据业务需求考虑分库：读写分离、热点隔离拆分，但是会引入分布式事务问题，以及跨库操作的难度</li></ul><p>要执行的操作：扣减库存、生成新订单、生成待支付订单、扣减优惠券、积分变动</p><p>库存表是数据库并发的瓶颈所在，需要在事务控制上做权衡：可以把扣减库存设置成一个独立的事务，其它操作成一个大的事务（订单、优惠券、积分操作），提高并发度，但是要做好额外的check</p><p>update 库存表 set 库存=库存-1 where id=** and 库存&gt;1</p><h3 id="答题验证码的设计" tabindex="-1">答题验证码的设计 <a class="header-anchor" href="#答题验证码的设计" aria-label="Permalink to &quot;答题验证码的设计&quot;">​</a></h3><ul><li>可以防止秒杀器的干扰，让更多用户有机会抢到</li><li>延缓请求，每个人的反应时间不同，把瞬间流量分散开来了</li></ul><p>验证码的设计可以分为2种：</p><ul><li>验证失败重新刷新答题（12306）：服务器交互量大，每错一次交互一次，但是可以大大降低秒杀器答题的可能性，因为没有试错这个功能，答题一直在变</li><li>验证失败提示失败，但是不刷新答题的算法：要么答题成功，进入下单界面，要么提示打错，继续答题（不刷新答题，无须交互，用js验证结果)。 这种方案，可以在加载题目的时候一起加载MD5加密的答案，然后后台再校验一遍，实现类似的防止作弊的效果。好处是不需要额外的服务器交互。 MD加密答案的算法里面要引入 userId PK这些因素进来来确保每次答案都不一样而且没有规律，避免秒杀器统计结果集</li></ul><p>答题的验证：除了验证答案的正确性意外，还要统计反应时间，例如12306的难题，正常人类的答题速度最快是1.5s，那么，小于1s的验证可以判定为机器验证</p><h3 id="注意事项" tabindex="-1">注意事项 <a class="header-anchor" href="#注意事项" aria-label="Permalink to &quot;注意事项&quot;">​</a></h3><p>为了提升并发，需要在事务上做妥协：</p><blockquote><p>单机上拆分事务：比如扣减库存表+(生成待支付订单+优惠券扣减+积分变动)是一个大的事务，为了提高并发，可以拆分为2个事务。分库以后引入分布式事务问题,为了保证用户体验，最好还是通过日志分析来人工维护，否则阻塞太严重，并发差。</p></blockquote></div></div></main><footer class="VPDocFooter" data-v-6b87e69f data-v-37656e44><!--[--><!--]--><!----><nav class="prev-next" data-v-37656e44><div class="pager" data-v-37656e44><a class="pager-link prev" href="/notebook/4%E3%80%81%E5%BE%AE%E6%9C%8D%E5%8A%A1/%E5%BF%85%E5%A4%87/%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81.html" data-v-37656e44><span class="desc" data-v-37656e44>Previous page</span><span class="title" data-v-37656e44>分布式锁</span></a></div><div class="pager" data-v-37656e44><a class="pager-link next" href="/notebook/%E4%B8%89%E9%AB%98/%E9%AB%98%E5%8F%AF%E7%94%A8.html" data-v-37656e44><span class="desc" data-v-37656e44>Next page</span><span class="title" data-v-37656e44>高可用</span></a></div></nav></footer><!--[--><!--]--></div></div></div><!--[--><!--]--></div></div><!----><!--[--><!--]--></div></div>
    <script>window.__VP_HASH_MAP__=JSON.parse("{\"2、数据库_mysql_mysql面试_基础.md\":\"40da680a\",\"1、学前端_5、小程序_小程序项目.md\":\"60a1629b\",\"1、学前端_4、node_知识篇.md\":\"a7fb500e\",\"1、学前端_2、js_ts_es6 进阶.md\":\"6d07ba10\",\"1、学前端_3、vue_vue3_vue3进阶.md\":\"7ac622b4\",\"5、运维_jenkins.md\":\"929081f8\",\"1、学前端_2、js_ts_typescript.md\":\"875a4aa4\",\"2、数据库_mysql_mysql核心_设计.md\":\"7faf46d1\",\"2、数据库_mysql_mysql核心_基础.md\":\"d8e97f3e\",\"1、学前端_1、html_css_html基础.md\":\"7584d076\",\"1、学前端_5、专题篇_问题篇.md\":\"e893aaa2\",\"2、数据库_mysql_mysql面试_进阶.md\":\"f934806d\",\"3、springboot_运维_原理.md\":\"f4a39db6\",\"2、数据库_influxdb.md\":\"6e1711e1\",\"3、springboot_新特性.md\":\"cdf3e307\",\"mybatis_mybatisplus_jpa.md\":\"8e41681b\",\"1、学前端_5、小程序_小程序优化.md\":\"a2185198\",\"2、数据库_redis_redis基础.md\":\"856df0e0\",\"linux_实用脚本.md\":\"f2299dd5\",\"4、微服务_必备_分布式基础.md\":\"d49863d5\",\"2、数据库_redis_redis优化.md\":\"e66ae32f\",\"4、微服务_springsecurity_进阶篇.md\":\"235a8e9e\",\"5、运维_chatgpt.md\":\"10db3823\",\"2、数据库_mysql_分库分表.md\":\"e1c8a095\",\"start.md\":\"9bc1ff8d\",\"5、运维_github.md\":\"2ec6c735\",\"java学前端_css.md\":\"f11b47f0\",\"1、学前端_5、专题篇_知识篇.md\":\"a463ed8d\",\"linux_软件部署.md\":\"d6722925\",\"2、数据库_neo4j.md\":\"97ad22ac\",\"team.md\":\"ce467a6a\",\"nginx_实战篇.md\":\"7785486e\",\"index.md\":\"8c3ec167\",\"计算机基础_计算机网络_网络基础.md\":\"7a54a85d\",\"1、学前端_4、node_进阶篇.md\":\"60f6db69\",\"java_java集合.md\":\"a049b313\",\"1、学前端_3、vue_vue3_vue3高级.md\":\"614d1516\",\"1、学前端_5、小程序_微信小程序.md\":\"9a4be771\",\"5、运维_netty.md\":\"12ca0278\",\"2、数据库_mysql_mysql核心_运维.md\":\"83f97c16\",\"idea_vs code.md\":\"afdcb593\",\"java学前端_vue3_组件.md\":\"1086884e\",\"idea_chrome.md\":\"4a32afbc\",\"云原生_k8s.md\":\"db58e65a\",\"2、数据库_mysql_mysql核心_进阶.md\":\"61d16dff\",\"ssm_springbatch.md\":\"f799ab4a\",\"三高_分布式.md\":\"db1b8a1b\",\"2、数据库_elasticsearch_1、es基础.md\":\"04d17448\",\"linux_linux基础.md\":\"4b0bf394\",\"idea_idea插件.md\":\"fa86e45a\",\"可视化 _ 监控_可视化大屏.md\":\"004553bd\",\"2、数据库_mongodb_整合.md\":\"3c47d7f4\",\"4、微服务_springsecurity_基础篇.md\":\"534a3401\",\"4、微服务_进阶.md\":\"69095c58\",\"计算机基础_计算机基础_操作系统.md\":\"0f75d113\",\"可视化 _ 监控_zabbix.md\":\"71f2270e\",\"nginx_基础篇.md\":\"c7d8bb50\",\"1、学前端_4、node_项目实战.md\":\"bc5065b8\",\"2、数据库_redis_redis原理.md\":\"5cedf685\",\"可视化 _ 监控_监控基础.md\":\"ac56ce4d\",\"三高_高并发.md\":\"ea9ffc99\",\"2、数据库_redis_redis高级.md\":\"1d5872f6\",\"1、学前端_4、node_基础篇.md\":\"581cc13a\",\"2、数据库_mongodb_基础.md\":\"fb7a0a29\",\"idea_idea基础.md\":\"6f2f9638\",\"4、微服务_必备_sentinel.md\":\"2edfbf6c\",\"2、数据库_elasticsearch_3、es高级.md\":\"ef146606\",\"1、学前端_3、vue_vue3_vue3新语法.md\":\"8afd5409\",\"消息中间件_canal.md\":\"3949163c\",\"ssm_maven.md\":\"2c5e12ed\",\"4、微服务_springsecurity_高级篇.md\":\"882d3ff3\",\"linux_linux进阶.md\":\"188ef7b4\",\"计算机基础_设计模式_uml.md\":\"634ba256\",\"计算机基础_算法_leetcode.md\":\"77162fb9\",\"项目实战_小兔鲜_进阶篇1.md\":\"17c52c81\",\"1、学前端_2、js_ts_es6 基础.md\":\"fda3f18b\",\"项目实战_小兔鲜_进阶篇2.md\":\"a0f23006\",\"软件测试_测试基础.md\":\"8c1060cd\",\"2、数据库_redis_本地缓存.md\":\"00617fe6\",\"nginx_面试篇.md\":\"e3fb373a\",\"mybatis_mybatisplus_mybatis.md\":\"9239e0ad\",\"linux_shell.md\":\"ae53d83b\",\"2、数据库_mysql_mysql核心_优化.md\":\"36230425\",\"项目实战_项目推荐.md\":\"f9d97630\",\"mybatis_mybatisplus_mybatisplus.md\":\"0030fd35\",\"项目实战_百度地图_进阶篇.md\":\"c8b93267\",\"三高_高可用.md\":\"323840c5\",\"java_java新特性.md\":\"22abf56d\",\"软件测试_压力测试.md\":\"9ab44440\",\"java学前端_html_js.md\":\"e0fcd240\",\"2、数据库_redis_redis实战.md\":\"d6daeeab\",\"nginx_进阶篇.md\":\"e6b63195\",\"三高_秒杀.md\":\"3878bb64\",\"5、运维_git.md\":\"0264925c\",\"java_java进阶.md\":\"e79cb5b4\",\"并发 _ 多线程_基础篇.md\":\"7adbfac5\",\"项目实战_百度地图_基础篇.md\":\"8afa5954\",\"java学前端_react.md\":\"3ec827dd\",\"1、学前端_1、html_css_css基础.md\":\"01b56712\",\"项目实战_小兔鲜_基础篇.md\":\"646f5df5\",\"1、学前端_2、js_ts_js 基础.md\":\"cb13e36f\",\"可视化 _ 监控_监控进阶.md\":\"0cdbc292\",\"计算机基础_设计模式_基础篇.md\":\"51617287\",\"计算机基础_数据结构_基础篇.md\":\"b2bfd8d4\",\"项目实战_苍穹外卖_进阶篇.md\":\"48415e41\",\"ssm_spring.md\":\"ab514659\",\"消息中间件_rabbitmq.md\":\"45b1eb28\",\"1、学前端_1、html_css_网页进阶.md\":\"db998248\",\"消息中间件_kafka.md\":\"b747dabf\",\"云原生_docker.md\":\"983c7ba7\",\"4、微服务_必备_分布式锁.md\":\"5af1cf8d\",\"消息中间件_rocketmq.md\":\"d441da85\",\"项目实战_黑马头条_基础篇.md\":\"b05af3a6\",\"ssm_springmvc.md\":\"81b9714f\",\"项目实战_支付.md\":\"1d7407dd\",\"项目实战_黑马头条_进阶篇2.md\":\"bff0015b\",\"项目实战_黑马头条_进阶篇.md\":\"19f18388\",\"java学前端_vue2_组件.md\":\"58c6b1df\",\"3、springboot_基础篇.md\":\"529c66f4\",\"3、springboot_应用篇.md\":\"8b92aa61\",\"项目实战_黑马头条_高级篇.md\":\"227c08c1\",\"1、学前端_5、小程序_uniapp.md\":\"71a282b4\",\"项目实战_云尚办公_基础篇.md\":\"1fe188ba\",\"并发 _ 多线程_并发完善.md\":\"26619c46\",\"1、学前端_2、js_ts_js 进阶.md\":\"657dfb8f\",\"java_java高级.md\":\"23782d1a\",\"java_java基础.md\":\"86d67c77\"}");window.__VP_SITE_DATA__=JSON.parse("{\"lang\":\"en-US\",\"dir\":\"ltr\",\"title\":\"VitePress\",\"description\":\"A VitePress site\",\"base\":\"/notebook/\",\"head\":[],\"appearance\":true,\"themeConfig\":{\"algolia\":{\"appId\":\"DW7O63I9IR\",\"apiKey\":\"f8ed758cdb288a8b06542bc35923c1a1\",\"indexName\":\"notebook\"},\"sidebar\":[{\"text\":\"Java\",\"collapsed\":true,\"items\":[{\"text\":\"Java基础\",\"link\":\"/Java/Java基础\"},{\"text\":\"Java新特性\",\"link\":\"/Java/Java新特性\"},{\"text\":\"Java进阶\",\"link\":\"/Java/Java进阶\"},{\"text\":\"Java集合\",\"link\":\"/Java/Java集合\"},{\"text\":\"Java高级\",\"link\":\"/Java/Java高级\"}]},{\"text\":\"Linux\",\"collapsed\":true,\"items\":[{\"text\":\"Linux基础\",\"link\":\"/Linux/Linux基础\"},{\"text\":\"Linux新特性\",\"link\":\"/Linux/Linux进阶\"},{\"text\":\"Shell脚本\",\"link\":\"/Linux/Shell\"},{\"text\":\"实用脚本\",\"link\":\"/Linux/实用脚本\"},{\"text\":\"软件部署\",\"link\":\"/Linux/软件部署\"}]},{\"text\":\"Nginx\",\"collapsed\":true,\"items\":[{\"text\":\"基础篇\",\"link\":\"/Nginx/基础篇\"},{\"text\":\"进阶篇\",\"link\":\"/Nginx/进阶篇\"},{\"text\":\"实战篇\",\"link\":\"/Nginx/实战篇\"},{\"text\":\"面试篇\",\"link\":\"/Nginx/面试篇\"}]},{\"text\":\"SSM\",\"collapsed\":true,\"items\":[{\"text\":\"Maven\",\"link\":\"/SSM/Maven\"},{\"text\":\"Spring\",\"link\":\"/SSM/Spring\"},{\"text\":\"SpringMVC\",\"link\":\"/SSM/SpringMVC\"},{\"text\":\"SpringBatch\",\"link\":\"/SSM/SpringBatch\"}]},{\"text\":\"SpringBoot\",\"collapsed\":true,\"items\":[{\"text\":\"基础篇\",\"link\":\"/3、SpringBoot/基础篇\"},{\"text\":\"应用篇\",\"link\":\"/3、SpringBoot/应用篇\"},{\"text\":\"新特性\",\"link\":\"/3、SpringBoot/新特性\"},{\"text\":\"运维&原理\",\"link\":\"/3、SpringBoot/运维&原理\"}]},{\"text\":\"SpringCloud\",\"collapsed\":true,\"items\":[{\"text\":\"SpringCloud\",\"link\":\"/4、微服务/进阶\"},{\"text\":\"Sentinel\",\"link\":\"/4、微服务/必备/Sentinel\"}]},{\"text\":\"SpringSecurity\",\"collapsed\":true,\"items\":[{\"text\":\"SpringSecurity基础篇\",\"link\":\"/4、微服务/SpringSecurity/基础篇\"},{\"text\":\"SpringSecurity进阶篇\",\"link\":\"/4、微服务/SpringSecurity/进阶篇\"},{\"text\":\"SpringSecurity高级篇\",\"link\":\"/4、微服务/SpringSecurity/高级篇\"}]},{\"text\":\"Mybatis & MybatisPlus\",\"collapsed\":true,\"items\":[{\"text\":\"Mybatis\",\"link\":\"/Mybatis&MybatisPlus/Mybatis\"},{\"text\":\"MybatisPlus\",\"link\":\"/Mybatis&MybatisPlus/MybatisPlus\"},{\"text\":\"JPA\",\"link\":\"/Mybatis&MybatisPlus/JPA\"}]},{\"text\":\"Git & ChatGPT\",\"collapsed\":true,\"items\":[{\"text\":\"Git\",\"link\":\"/5、运维/Git\"},{\"text\":\"Github\",\"link\":\"/5、运维/Github\"},{\"text\":\"ChatGPT\",\"link\":\"/5、运维/ChatGPT\"},{\"text\":\"Jenkins\",\"link\":\"/5、运维/Jenkins\"},{\"text\":\"Netty\",\"link\":\"/5、运维/Netty\"}]},{\"text\":\"数据库\",\"collapsed\":true,\"items\":[{\"text\":\"MySQL\",\"collapsed\":true,\"items\":[{\"text\":\"MySQL基础\",\"link\":\"/2、数据库/MySQL/MySQL核心/基础\"},{\"text\":\"MySQL进阶\",\"link\":\"/2、数据库/MySQL/MySQL核心/进阶\"},{\"text\":\"MySQL优化\",\"link\":\"/2、数据库/MySQL/MySQL核心/优化\"},{\"text\":\"MySQL设计\",\"link\":\"/2、数据库/MySQL/MySQL核心/设计\"},{\"text\":\"MySQL运维\",\"link\":\"/2、数据库/MySQL/MySQL核心/运维\"},{\"text\":\"分库分表\",\"link\":\"/2、数据库/MySQL/分库分表\"}]},{\"text\":\"Redis\",\"collapsed\":true,\"items\":[{\"text\":\"Redis基础\",\"link\":\"/2、数据库/Redis/Redis基础\"},{\"text\":\"Redis优化\",\"link\":\"/2、数据库/Redis/Redis优化\"},{\"text\":\"Redis原理\",\"link\":\"/2、数据库/Redis/Redis原理\"},{\"text\":\"Redis高级\",\"link\":\"/2、数据库/Redis/Redis高级\"},{\"text\":\"Redis实战\",\"link\":\"/2、数据库/Redis/Redis实战\"},{\"text\":\"本地缓存\",\"link\":\"/2、数据库/Redis/本地缓存\"}]},{\"text\":\"MongoDB\",\"collapsed\":true,\"items\":[{\"text\":\"MongoDB基础\",\"link\":\"/2、数据库/MongoDB/基础\"},{\"text\":\"MongoDB进阶\",\"link\":\"/2、数据库/MongoDB/整合\"}]},{\"text\":\"ElasticSearch\",\"collapsed\":true,\"items\":[{\"text\":\"ES基础\",\"link\":\"/2、数据库/ElasticSearch/1、ES基础\"},{\"text\":\"ES高级\",\"link\":\"/2、数据库/ElasticSearch/3、ES高级\"}]},{\"text\":\"InfluxDB\",\"link\":\"/2、数据库/influxdb\"},{\"text\":\"Neo4j\",\"link\":\"/2、数据库/Neo4j\"}]},{\"text\":\"高并发 & 秒杀 & 分布式\",\"collapsed\":true,\"items\":[{\"text\":\"分布式理论\",\"link\":\"/三高/分布式\"},{\"text\":\"分布式锁\",\"link\":\"/4、微服务/必备/分布式锁\"},{\"text\":\"秒杀\",\"link\":\"/三高/秒杀\"},{\"text\":\"高可用\",\"link\":\"/三高/高可用\"},{\"text\":\"高并发\",\"link\":\"/三高/高并发\"}]},{\"text\":\"云原生\",\"collapsed\":true,\"items\":[{\"text\":\"Docker\",\"link\":\"/云原生/Docker\"},{\"text\":\"K8S\",\"link\":\"/云原生/K8S\"}]},{\"text\":\"可视化 & 监控\",\"collapsed\":true,\"items\":[{\"text\":\"监控基础\",\"link\":\"/可视化 & 监控/监控基础\"},{\"text\":\"监控进阶\",\"link\":\"/可视化 & 监控/监控进阶\"},{\"text\":\"可视化大屏\",\"link\":\"/可视化 & 监控/可视化大屏\"},{\"text\":\"Zabbix\",\"link\":\"/可视化 & 监控/Zabbix\"}]},{\"text\":\"学前端\",\"collapsed\":true,\"items\":[{\"text\":\"HTML+CSS\",\"collapsed\":true,\"items\":[{\"text\":\"HTML基础\",\"link\":\"/1、学前端/1、HTML+CSS/HTML基础\"},{\"text\":\"CSS基础\",\"link\":\"/1、学前端/1、HTML+CSS/CSS基础\"},{\"text\":\"网页进阶\",\"link\":\"/1、学前端/1、HTML+CSS/网页进阶\"}]},{\"text\":\"JS+TS\",\"collapsed\":true,\"items\":[{\"text\":\"JS基础\",\"link\":\"/1、学前端/2、JS+TS/JS 基础\"},{\"text\":\"JS进阶\",\"link\":\"/1、学前端/2、JS+TS/JS 进阶\"},{\"text\":\"ES6基础\",\"link\":\"/1、学前端/2、JS+TS/ES6 基础\"},{\"text\":\"ES6进阶\",\"link\":\"/1、学前端/2、JS+TS/ES6 进阶\"},{\"text\":\"TS基础\",\"link\":\"/1、学前端/2、JS+TS/TypeScript\"}]},{\"text\":\"NodeJS\",\"collapsed\":true,\"items\":[{\"text\":\"Node基础\",\"link\":\"/1、学前端/4、Node/基础篇\"},{\"text\":\"Node进阶\",\"link\":\"/1、学前端/4、Node/进阶篇\"},{\"text\":\"项目实战\",\"link\":\"/1、学前端/4、Node/项目实战\"}]},{\"text\":\"Vue\",\"collapsed\":true,\"items\":[{\"text\":\"Vue3进阶\",\"link\":\"/1、学前端/3、Vue/Vue3/Vue3进阶\"},{\"text\":\"Vue3高级\",\"link\":\"/1、学前端/3、Vue/Vue3/Vue3高级\"},{\"text\":\"Vue3新语法\",\"link\":\"/1、学前端/3、Vue/Vue3/Vue3新语法\"},{\"text\":\"项目实战\",\"link\":\"/1、学前端/3、Vue/Vue2/Vue2项目\"}]},{\"text\":\"小程序\",\"collapsed\":true,\"items\":[{\"text\":\"小程序基础\",\"link\":\"/1、学前端/5、小程序/微信小程序\"},{\"text\":\"小程序优化\",\"link\":\"/1、学前端/5、小程序/小程序优化\"},{\"text\":\"uniapp\",\"link\":\"/1、学前端/5、小程序/uniapp\"},{\"text\":\"项目实战\",\"link\":\"/1、学前端/5、小程序/小程序项目\"}]}]},{\"text\":\"计算机基础\",\"collapsed\":true,\"items\":[{\"text\":\"数据结构\",\"link\":\"/计算机基础/数据结构/基础篇\"},{\"text\":\"操作系统\",\"link\":\"/计算机基础/计算机基础/操作系统\"},{\"text\":\"设计模式\",\"link\":\"/计算机基础/设计模式/基础篇\"},{\"text\":\"计算机网络\",\"link\":\"/计算机基础/计算机网络/网络基础\"},{\"text\":\"UML\",\"link\":\"/计算机基础/设计模式/UML\"},{\"text\":\"LeetCode\",\"link\":\"/计算机基础/算法/LeetCode\"}]},{\"text\":\"项目实战\",\"collapsed\":true,\"items\":[{\"text\":\"云尚办公\",\"collapsed\":true,\"items\":[{\"text\":\"基础篇\",\"link\":\"/项目实战/云尚办公/基础篇\"}]},{\"text\":\"小兔鲜\",\"collapsed\":true,\"items\":[{\"text\":\"基础篇\",\"link\":\"/项目实战/小兔鲜/基础篇\"},{\"text\":\"进阶篇1\",\"link\":\"/项目实战/小兔鲜/进阶篇1\"},{\"text\":\"进阶篇2\",\"link\":\"/项目实战/小兔鲜/进阶篇2\"}]},{\"text\":\"地图\",\"collapsed\":true,\"items\":[{\"text\":\"基础篇\",\"link\":\"/项目实战/百度地图/基础篇\"},{\"text\":\"进阶篇\",\"link\":\"/项目实战/百度地图/进阶篇\"}]},{\"text\":\"苍穹外卖\",\"collapsed\":true,\"items\":[{\"text\":\"进阶篇\",\"link\":\"/项目实战/苍穹外卖/进阶篇\"}]},{\"text\":\"黑马头条\",\"collapsed\":true,\"items\":[{\"text\":\"基础篇\",\"link\":\"/项目实战/黑马头条/基础篇\"},{\"text\":\"进阶篇\",\"link\":\"/项目实战/黑马头条/进阶篇\"},{\"text\":\"进阶篇2\",\"link\":\"/项目实战/黑马头条/进阶篇2\"},{\"text\":\"高级篇\",\"link\":\"/项目实战/黑马头条/高级篇\"}]},{\"text\":\"支付\",\"link\":\"/项目实战/支付\"},{\"text\":\"项目推荐\",\"link\":\"/项目实战/项目推荐\"}]},{\"text\":\"团队成员\",\"link\":\"/team\"}],\"siteTitle\":\"任硕的文档\",\"logo\":\"/Vue.png\",\"nav\":[{\"text\":\"Java学前端\",\"items\":[{\"items\":[{\"text\":\"HTML+JS\",\"link\":\"/Java学前端/HTML+JS\"},{\"text\":\"CSS\",\"link\":\"/Java学前端/CSS\"},{\"text\":\"Vue2+组件\",\"link\":\"/Java学前端/Vue2+组件\"},{\"text\":\"Vue3+组件\",\"link\":\"/Java学前端/Vue3+组件\"},{\"text\":\"React\",\"link\":\"/Java学前端/React\"}]}],\"activeMatch\":\"/Java/\"},{\"text\":\"软件测试\",\"items\":[{\"items\":[{\"text\":\"测试基础\",\"link\":\"/软件测试/测试基础\"},{\"text\":\"压力测试\",\"link\":\"/软件测试/压力测试\"}]}]},{\"text\":\"多线程\",\"items\":[{\"items\":[{\"text\":\"基础篇\",\"link\":\"/并发 & 多线程/基础篇\"},{\"text\":\"进阶篇\",\"link\":\"/并发 & 多线程/并发完善\"}]}]},{\"text\":\"开发工具\",\"items\":[{\"items\":[{\"text\":\"Chrome\",\"link\":\"/IDEA/Chrome\"},{\"text\":\"IDEA基础\",\"link\":\"/IDEA/IDEA基础\"},{\"text\":\"IDEA插件\",\"link\":\"/IDEA/IDEA插件\"},{\"text\":\"VS Code\",\"link\":\"/IDEA/VS Code\"}]}]},{\"text\":\"消息中间件\",\"items\":[{\"items\":[{\"text\":\"RabbitMQ\",\"link\":\"/消息中间件/RabbitMQ\"},{\"text\":\"RocketMQ\",\"link\":\"/消息中间件/RocketMQ\"},{\"text\":\"Kafka\",\"link\":\"/消息中间件/Kafka\"},{\"text\":\"Canal\",\"link\":\"/消息中间件/Canal\"}]}]}],\"socialLinks\":[{\"icon\":\"github\",\"link\":\"https://github.com/renshuo123/renshuo123.github.io\"},{\"icon\":\"twitter\",\"link\":\"#\"},{\"icon\":{\"svg\":\"<svg t=\\\"1676028692954\\\" class=\\\"icon\\\" ...</path></svg>\"},\"link\":\"https://github.com/\"}]},\"locales\":{},\"scrollOffset\":90,\"cleanUrls\":false}");</script>
    
  </body>
</html>